20219: Rearrange non-sortable logs to top of merged all logs view
[arvados.git] / cypress / integration / process.spec.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 describe('Process tests', function() {
6     let activeUser;
7     let adminUser;
8
9     before(function() {
10         // Only set up common users once. These aren't set up as aliases because
11         // aliases are cleaned up after every test. Also it doesn't make sense
12         // to set the same users on beforeEach() over and over again, so we
13         // separate a little from Cypress' 'Best Practices' here.
14         cy.getUser('admin', 'Admin', 'User', true, true)
15             .as('adminUser').then(function() {
16                 adminUser = this.adminUser;
17             }
18         );
19         cy.getUser('user', 'Active', 'User', false, true)
20             .as('activeUser').then(function() {
21                 activeUser = this.activeUser;
22             }
23         );
24     });
25
26     beforeEach(function() {
27         cy.clearCookies();
28         cy.clearLocalStorage();
29     });
30
31     function setupDockerImage(image_name) {
32         // Create a collection that will be used as a docker image for the tests.
33         cy.createCollection(adminUser.token, {
34             name: 'docker_image',
35             manifest_text: ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n"
36         }).as('dockerImage').then(function(dockerImage) {
37             // Give read permissions to the active user on the docker image.
38             cy.createLink(adminUser.token, {
39                 link_class: 'permission',
40                 name: 'can_read',
41                 tail_uuid: activeUser.user.uuid,
42                 head_uuid: dockerImage.uuid
43             }).as('dockerImagePermission').then(function() {
44                 // Set-up docker image collection tags
45                 cy.createLink(activeUser.token, {
46                     link_class: 'docker_image_repo+tag',
47                     name: image_name,
48                     head_uuid: dockerImage.uuid,
49                 }).as('dockerImageRepoTag');
50                 cy.createLink(activeUser.token, {
51                     link_class: 'docker_image_hash',
52                     name: 'sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678',
53                     head_uuid: dockerImage.uuid,
54                 }).as('dockerImageHash');
55             })
56         });
57         return cy.getAll('@dockerImage', '@dockerImageRepoTag', '@dockerImageHash',
58             '@dockerImagePermission').then(function([dockerImage]) {
59                 return dockerImage;
60             });
61     }
62
63     function createContainerRequest(user, name, docker_image, command, reuse = false, state = 'Uncommitted') {
64         return setupDockerImage(docker_image).then(function(dockerImage) {
65             return cy.createContainerRequest(user.token, {
66                 name: name,
67                 command: command,
68                 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
69                 output_path: 'stdout.txt',
70                 priority: 1,
71                 runtime_constraints: {
72                     vcpus: 1,
73                     ram: 1,
74                 },
75                 use_existing: reuse,
76                 state: state,
77                 mounts: {
78                     foo: {
79                         kind: 'tmp',
80                         path: '/tmp/foo',
81                     }
82                 }
83             });
84         });
85     }
86
87     describe('Details panel', function() {
88         it('shows process details', function() {
89             createContainerRequest(
90                 activeUser,
91                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
92                 'arvados/jobs',
93                 ['echo', 'hello world'],
94                 false, 'Committed')
95             .then(function(containerRequest) {
96                 cy.loginAs(activeUser);
97                 cy.goToPath(`/processes/${containerRequest.uuid}`);
98                 cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
99                 cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
100                 cy.get('[data-cy=process-details-attributes-runtime-user]').should('not.exist');
101             });
102
103             // Fake submitted by another user
104             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
105                 req.reply((res) => {
106                     res.body.modified_by_user_uuid = 'zzzzz-tpzed-000000000000000';
107                 });
108             });
109
110             createContainerRequest(
111                 activeUser,
112                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
113                 'arvados/jobs',
114                 ['echo', 'hello world'],
115                 false, 'Committed')
116             .then(function(containerRequest) {
117                 cy.loginAs(activeUser);
118                 cy.goToPath(`/processes/${containerRequest.uuid}`);
119                 cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
120                 cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`zzzzz-tpzed-000000000000000`);
121                 cy.get('[data-cy=process-details-attributes-runtime-user]').contains(`Active User (${activeUser.user.uuid})`);
122             });
123         });
124
125         it('should show runtime status indicators', function() {
126             // Setup running container with runtime_status error & warning messages
127             createContainerRequest(
128                 activeUser,
129                 'test_container_request',
130                 'arvados/jobs',
131                 ['echo', 'hello world'],
132                 false, 'Committed')
133             .as('containerRequest')
134             .then(function(containerRequest) {
135                 expect(containerRequest.state).to.equal('Committed');
136                 expect(containerRequest.container_uuid).not.to.be.equal('');
137
138                 cy.getContainer(activeUser.token, containerRequest.container_uuid)
139                 .then(function(queuedContainer) {
140                     expect(queuedContainer.state).to.be.equal('Queued');
141                 });
142                 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
143                     state: 'Locked'
144                 }).then(function(lockedContainer) {
145                     expect(lockedContainer.state).to.be.equal('Locked');
146
147                     cy.updateContainer(adminUser.token, lockedContainer.uuid, {
148                         state: 'Running',
149                         runtime_status: {
150                             error: 'Something went wrong',
151                             errorDetail: 'Process exited with status 1',
152                             warning: 'Free disk space is low',
153                         }
154                     })
155                     .as('runningContainer')
156                     .then(function(runningContainer) {
157                         expect(runningContainer.state).to.be.equal('Running');
158                         expect(runningContainer.runtime_status).to.be.deep.equal({
159                             'error': 'Something went wrong',
160                             'errorDetail': 'Process exited with status 1',
161                             'warning': 'Free disk space is low',
162                         });
163                     });
164                 })
165             });
166             // Test that the UI shows the error and warning messages
167             cy.getAll('@containerRequest', '@runningContainer').then(function([containerRequest]) {
168                 cy.loginAs(activeUser);
169                 cy.goToPath(`/processes/${containerRequest.uuid}`);
170                 cy.get('[data-cy=process-runtime-status-error]')
171                     .should('contain', 'Something went wrong')
172                     .and('contain', 'Process exited with status 1');
173                 cy.get('[data-cy=process-runtime-status-warning]')
174                     .should('contain', 'Free disk space is low')
175                     .and('contain', 'No additional warning details available');
176             });
177
178
179             // Force container_count for testing
180             let containerCount = 2;
181             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
182                 req.reply((res) => {
183                     res.body.container_count = containerCount;
184                 });
185             });
186
187             cy.getAll('@containerRequest').then(function([containerRequest]) {
188                 cy.goToPath(`/processes/${containerRequest.uuid}`);
189                 cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
190                     .should('contain', 'Process retried 1 time');
191             });
192
193             cy.getAll('@containerRequest').then(function([containerRequest]) {
194                 containerCount = 3;
195                 cy.goToPath(`/processes/${containerRequest.uuid}`);
196                 cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
197                     .should('contain', 'Process retried 2 times');
198             });
199         });
200
201         it('allows copying processes', function() {
202             const crName = 'first_container_request';
203             const copiedCrName = 'copied_container_request';
204             createContainerRequest(
205                 activeUser,
206                 crName,
207                 'arvados/jobs',
208                 ['echo', 'hello world'],
209                 false, 'Committed')
210             .then(function(containerRequest) {
211                 cy.loginAs(activeUser);
212                 cy.goToPath(`/processes/${containerRequest.uuid}`);
213                 cy.get('[data-cy=process-details]').should('contain', crName);
214
215                 cy.get('[data-cy=process-details]').find('button[title="More options"]').click();
216                 cy.get('ul[data-cy=context-menu]').contains("Copy and re-run process").click();
217             });
218
219             cy.get('[data-cy=form-dialog]').within(() => {
220                 cy.get('input[name=name]').clear().type(copiedCrName);
221                 cy.get('[data-cy=projects-tree-home-tree-picker]').click();
222                 cy.get('[data-cy=form-submit-btn]').click();
223             });
224
225             cy.get('[data-cy=process-details]').should('contain', copiedCrName);
226             cy.get('[data-cy=process-details]').find('button').contains('Run');
227         });
228
229         const getFakeContainer = (fakeContainerUuid) => ({
230             href: `/containers/${fakeContainerUuid}`,
231             kind: "arvados#container",
232             etag: "ecfosljpnxfari9a8m7e4yv06",
233             uuid: fakeContainerUuid,
234             owner_uuid: "zzzzz-tpzed-000000000000000",
235             created_at: "2023-02-13T15:55:47.308915000Z",
236             modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
237             modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
238             modified_at: "2023-02-15T19:12:45.987086000Z",
239             command: [
240             "arvados-cwl-runner",
241             "--api=containers",
242             "--local",
243             "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
244             "/var/lib/cwl/workflow.json#main",
245             "/var/lib/cwl/cwl.input.json",
246             ],
247             container_image: "4ad7d11381df349e464694762db14e04+303",
248             cwd: "/var/spool/cwl",
249             environment: {},
250             exit_code: null,
251             finished_at: null,
252             locked_by_uuid: null,
253             log: null,
254             output: null,
255             output_path: "/var/spool/cwl",
256             progress: null,
257             runtime_constraints: {
258             API: true,
259             cuda: {
260                 device_count: 0,
261                 driver_version: "",
262                 hardware_capability: "",
263             },
264             keep_cache_disk: 2147483648,
265             keep_cache_ram: 0,
266             ram: 1342177280,
267             vcpus: 1,
268             },
269             runtime_status: {},
270             started_at: null,
271             auth_uuid: null,
272             scheduling_parameters: {
273             max_run_time: 0,
274             partitions: [],
275             preemptible: false,
276             },
277             runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
278             runtime_auth_scopes: ["all"],
279             lock_count: 2,
280             gateway_address: null,
281             interactive_session_started: false,
282             output_storage_classes: ["default"],
283             output_properties: {},
284             cost: 0.0,
285             subrequests_cost: 0.0,
286         });
287
288         it('shows cancel button when appropriate', function() {
289             // Ignore collection requests
290             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/*`}, {
291                 statusCode: 200,
292                 body: {}
293             });
294
295             // Uncommitted container
296             const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
297             createContainerRequest(
298                 activeUser,
299                 crUncommitted,
300                 'arvados/jobs',
301                 ['echo', 'hello world'],
302                 false, 'Uncommitted')
303             .then(function(containerRequest) {
304                 // Navigate to process and verify run / cancel button
305                 cy.goToPath(`/processes/${containerRequest.uuid}`);
306                 cy.waitForDom();
307                 cy.get('[data-cy=process-details]').should('contain', crUncommitted);
308                 cy.get('[data-cy=process-run-button]').should('exist');
309                 cy.get('[data-cy=process-cancel-button]').should('not.exist');
310             });
311
312             // Queued container
313             const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
314             const fakeCrUuid = 'zzzzz-dz642-000000000000001';
315             createContainerRequest(
316                 activeUser,
317                 crQueued,
318                 'arvados/jobs',
319                 ['echo', 'hello world'],
320                 false, 'Committed')
321             .then(function(containerRequest) {
322                 // Fake container uuid
323                 cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
324                     req.reply((res) => {
325                         res.body.output_uuid = fakeCrUuid;
326                         res.body.priority = 500;
327                         res.body.state = "Committed";
328                     });
329                 });
330
331                 // Fake container
332                 const container = getFakeContainer(fakeCrUuid);
333                 cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrUuid}`}, {
334                     statusCode: 200,
335                     body: {...container, state: "Queued", priority: 500}
336                 });
337
338                 // Navigate to process and verify cancel button
339                 cy.goToPath(`/processes/${containerRequest.uuid}`);
340                 cy.waitForDom();
341                 cy.get('[data-cy=process-details]').should('contain', crQueued);
342                 cy.get('[data-cy=process-cancel-button]').contains('Cancel');
343             });
344
345             // Locked container
346             const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
347             const fakeCrLockedUuid = 'zzzzz-dz642-000000000000002';
348             createContainerRequest(
349                 activeUser,
350                 crLocked,
351                 'arvados/jobs',
352                 ['echo', 'hello world'],
353                 false, 'Committed')
354             .then(function(containerRequest) {
355                 // Fake container uuid
356                 cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
357                     req.reply((res) => {
358                         res.body.output_uuid = fakeCrLockedUuid;
359                         res.body.priority = 500;
360                         res.body.state = "Committed";
361                     });
362                 });
363
364                 // Fake container
365                 const container = getFakeContainer(fakeCrLockedUuid);
366                 cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrLockedUuid}`}, {
367                     statusCode: 200,
368                     body: {...container, state: "Locked", priority: 500}
369                 });
370
371                 // Navigate to process and verify cancel button
372                 cy.goToPath(`/processes/${containerRequest.uuid}`);
373                 cy.waitForDom();
374                 cy.get('[data-cy=process-details]').should('contain', crLocked);
375                 cy.get('[data-cy=process-cancel-button]').contains('Cancel');
376             });
377
378             // On Hold container
379             const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
380             const fakeCrOnHoldUuid = 'zzzzz-dz642-000000000000003';
381             createContainerRequest(
382                 activeUser,
383                 crOnHold,
384                 'arvados/jobs',
385                 ['echo', 'hello world'],
386                 false, 'Committed')
387             .then(function(containerRequest) {
388                 // Fake container uuid
389                 cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
390                     req.reply((res) => {
391                         res.body.output_uuid = fakeCrOnHoldUuid;
392                         res.body.priority = 0;
393                         res.body.state = "Committed";
394                     });
395                 });
396
397                 // Fake container
398                 const container = getFakeContainer(fakeCrOnHoldUuid);
399                 cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrOnHoldUuid}`}, {
400                     statusCode: 200,
401                     body: {...container, state: "Queued", priority: 0}
402                 });
403
404                 // Navigate to process and verify cancel button
405                 cy.goToPath(`/processes/${containerRequest.uuid}`);
406                 cy.waitForDom();
407                 cy.get('[data-cy=process-details]').should('contain', crOnHold);
408                 cy.get('[data-cy=process-run-button]').should('exist');
409                 cy.get('[data-cy=process-cancel-button]').should('not.exist');
410             });
411         });
412
413     });
414
415
416     describe('Logs panel', function() {
417         it('shows live process logs', function() {
418             const crName = 'test_container_request';
419             createContainerRequest(
420                 activeUser,
421                 crName,
422                 'arvados/jobs',
423                 ['echo', 'hello world'],
424                 false, 'Committed')
425             .then(function(containerRequest) {
426                 cy.loginAs(activeUser);
427                 cy.goToPath(`/processes/${containerRequest.uuid}`);
428                 cy.get('[data-cy=process-details]').should('contain', crName);
429                 cy.get('[data-cy=process-logs]')
430                     .should('contain', 'No logs yet')
431                     .and('not.contain', 'hello world');
432
433                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
434                     "2023-07-18T20:14:48.128642814Z hello world"
435                 ]).then(() => {
436                     cy.get('[data-cy=process-logs]', {timeout: 7000})
437                         .should('not.contain', 'No logs yet')
438                         .and('contain', 'hello world');
439                 });
440
441                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
442                     "2023-07-18T20:14:49.128642814Z hello new line"
443                 ]).then(() => {
444                     cy.get('[data-cy=process-logs]', {timeout: 7000})
445                         .should('not.contain', 'No logs yet')
446                         .and('contain', 'hello new line');
447                 });
448             });
449         });
450
451         it('filters process logs by event type', function() {
452             const nodeInfoLogs = [
453                 'Host Information',
454                 'Linux compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p 5.4.0-1059-azure #62~18.04.1-Ubuntu SMP Tue Sep 14 17:53:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux',
455                 'CPU Information',
456                 'processor  : 0',
457                 'vendor_id  : GenuineIntel',
458                 'cpu family : 6',
459                 'model      : 79',
460                 'model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz'
461             ];
462             const crunchRunLogs = [
463                 '2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection',
464                 '2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started',
465                 '2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)',
466                 '2022-03-22T13:56:26.244862836Z Executing container \'zzzzz-dz642-1wokwvcct9s9du3\' using docker runtime',
467                 '2022-03-22T13:56:26.245037738Z Executing on host \'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p\'',
468             ];
469             const stdoutLogs = [
470                 '2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.',
471                 '2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.',
472                 '2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.',
473                 '2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.',
474                 '2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.',
475                 '2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.',
476                 '2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.',
477                 '2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.',
478                 '2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.',
479                 '2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.',
480                 '2022-03-22T13:56:22.542418097Z Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.',
481                 '2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.',
482                 '2022-03-22T13:56:22.542418117Z Sed at enim sit amet nulla tincidunt mattis. Aenean eget aliquet ex, non ultrices ex. Nulla ex tortor, vestibulum aliquam tempor ac, aliquam vel est.',
483                 '2022-03-22T13:56:22.542418127Z Fusce auctor faucibus libero id venenatis. Etiam sodales, odio eu cursus efficitur, quam sem blandit ex, quis porttitor enim dui quis lectus. In id tincidunt felis.',
484                 '2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.',
485                 '2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.',
486                 '2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.',
487                 '2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.',
488             ];
489
490             createContainerRequest(
491                 activeUser,
492                 'test_container_request',
493                 'arvados/jobs',
494                 ['echo', 'hello world'],
495                 false, 'Committed')
496             .then(function(containerRequest) {
497                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as('nodeInfoLogs');
498                 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as('crunchRunLogs');
499                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as('stdoutLogs');
500
501                 cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
502                     cy.loginAs(activeUser);
503                     cy.goToPath(`/processes/${containerRequest.uuid}`);
504                     // Should show main logs by default
505                     cy.get('[data-cy=process-logs-filter]', {timeout: 7000}).should('contain', 'Main logs');
506                     cy.get('[data-cy=process-logs]')
507                         .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
508                         .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
509                         .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
510                     // Select 'All logs'
511                     cy.get('[data-cy=process-logs-filter]').click();
512                     cy.get('body').contains('li', 'All logs').click();
513                     cy.get('[data-cy=process-logs]')
514                         .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
515                         .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
516                         .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
517                     // Select 'node-info' logs
518                     cy.get('[data-cy=process-logs-filter]').click();
519                     cy.get('body').contains('li', 'node-info').click();
520                     cy.get('[data-cy=process-logs]')
521                         .should('not.contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
522                         .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
523                         .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
524                     // Select 'stdout' logs
525                     cy.get('[data-cy=process-logs-filter]').click();
526                     cy.get('body').contains('li', 'stdout').click();
527                     cy.get('[data-cy=process-logs]')
528                         .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
529                         .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
530                         .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
531                 });
532             });
533         });
534
535         it('sorts combined logs', function() {
536             const crName = 'test_container_request';
537             createContainerRequest(
538                 activeUser,
539                 crName,
540                 'arvados/jobs',
541                 ['echo', 'hello world'],
542                 false, 'Committed')
543             .then(function(containerRequest) {
544                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
545                     "3: nodeinfo 1",
546                     "2: nodeinfo 2",
547                     "1: nodeinfo 3",
548                     "2: nodeinfo 4",
549                     "3: nodeinfo 5",
550                 ]).as('node-info');
551
552                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
553                     "2023-07-18T20:14:48.128642814Z first",
554                     "2023-07-18T20:14:49.128642814Z third"
555                 ]).as('stdout');
556
557                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
558                     "2023-07-18T20:14:48.528642814Z second"
559                 ]).as('stderr');
560
561                 cy.loginAs(activeUser);
562                 cy.goToPath(`/processes/${containerRequest.uuid}`);
563                 cy.get('[data-cy=process-details]').should('contain', crName);
564                 cy.get('[data-cy=process-logs]')
565                     .should('contain', 'No logs yet');
566
567                 cy.getAll('@node-info', '@stdout', '@stderr').then(() => {
568                     // Verify sorted main logs
569                     cy.get('[data-cy=process-logs] pre', {timeout: 7000})
570                         .eq(0).should('contain', '2023-07-18T20:14:48.128642814Z first');
571                     cy.get('[data-cy=process-logs] pre')
572                         .eq(1).should('contain', '2023-07-18T20:14:48.528642814Z second');
573                     cy.get('[data-cy=process-logs] pre')
574                         .eq(2).should('contain', '2023-07-18T20:14:49.128642814Z third');
575
576                     // Switch to All logs
577                     cy.get('[data-cy=process-logs-filter]').click();
578                     cy.get('body').contains('li', 'All logs').click();
579                     // Verify non-sorted lines were preserved
580                     cy.get('[data-cy=process-logs] pre')
581                         .eq(0).should('contain', '3: nodeinfo 1');
582                     cy.get('[data-cy=process-logs] pre')
583                         .eq(1).should('contain', '2: nodeinfo 2');
584                     cy.get('[data-cy=process-logs] pre')
585                         .eq(2).should('contain', '1: nodeinfo 3');
586                     cy.get('[data-cy=process-logs] pre')
587                         .eq(3).should('contain', '2: nodeinfo 4');
588                     cy.get('[data-cy=process-logs] pre')
589                         .eq(4).should('contain', '3: nodeinfo 5');
590                     // Verify sorted logs
591                     cy.get('[data-cy=process-logs] pre')
592                         .eq(5).should('contain', '2023-07-18T20:14:48.128642814Z first');
593                     cy.get('[data-cy=process-logs] pre')
594                         .eq(6).should('contain', '2023-07-18T20:14:48.528642814Z second');
595                     cy.get('[data-cy=process-logs] pre')
596                         .eq(7).should('contain', '2023-07-18T20:14:49.128642814Z third');
597                 });
598             });
599         });
600
601         it('correctly generates sniplines', function() {
602             const SNIPLINE = `================ âœ€ ================ âœ€ ========= Some log(s) were skipped ========= âœ€ ================ âœ€ ================`;
603             const crName = 'test_container_request';
604             createContainerRequest(
605                 activeUser,
606                 crName,
607                 'arvados/jobs',
608                 ['echo', 'hello world'],
609                 false, 'Committed')
610             .then(function(containerRequest) {
611
612                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
613                     'X'.repeat(63999) + '_' +
614                     'O'.repeat(100) +
615                     '_' + 'X'.repeat(63999)
616                 ]).as('stdout');
617
618                 cy.loginAs(activeUser);
619                 cy.goToPath(`/processes/${containerRequest.uuid}`);
620                 cy.get('[data-cy=process-details]').should('contain', crName);
621                 cy.get('[data-cy=process-logs]')
622                     .should('contain', 'No logs yet');
623
624                 // Switch to stdout since lines are unsortable (no timestamp)
625                 cy.get('[data-cy=process-logs-filter]').click();
626                 cy.get('body').contains('li', 'stdout').click();
627
628                 cy.getAll('@stdout').then(() => {
629                     // Verify first 64KB and snipline
630                     cy.get('[data-cy=process-logs] pre', {timeout: 7000})
631                         .eq(0).should('contain', 'X'.repeat(63999) + '_\n' + SNIPLINE);
632                     // Verify last 64KB
633                     cy.get('[data-cy=process-logs] pre')
634                         .eq(1).should('contain', '_' + 'X'.repeat(63999));
635                     // Verify none of the Os got through
636                     cy.get('[data-cy=process-logs] pre')
637                         .should('not.contain', 'O');
638                 });
639             });
640         });
641
642     });
643
644     describe('I/O panel', function() {
645         const testInputs = [
646             {
647                 definition: {
648                     "id": "#main/input_file",
649                     "label": "Label Description",
650                     "type": "File"
651                 },
652                 input: {
653                     "input_file": {
654                         "basename": "input1.tar",
655                         "class": "File",
656                         "location": "keep:00000000000000000000000000000000+01/input1.tar",
657                         "secondaryFiles": [
658                             {
659                                 "basename": "input1-2.txt",
660                                 "class": "File",
661                                 "location": "keep:00000000000000000000000000000000+01/input1-2.txt"
662                             },
663                             {
664                                 "basename": "input1-3.txt",
665                                 "class": "File",
666                                 "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
667                             },
668                             {
669                                 "basename": "input1-4.txt",
670                                 "class": "File",
671                                 "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
672                             }
673                         ]
674                     }
675                 }
676             },
677             {
678                 definition: {
679                     "id": "#main/input_dir",
680                     "doc": "Doc Description",
681                     "type": "Directory"
682                 },
683                 input: {
684                     "input_dir": {
685                         "basename": "11111111111111111111111111111111+01",
686                         "class": "Directory",
687                         "location": "keep:11111111111111111111111111111111+01"
688                     }
689                 }
690             },
691             {
692                 definition: {
693                     "id": "#main/input_bool",
694                     "doc": ["Doc desc 1", "Doc desc 2"],
695                     "type": "boolean"
696                 },
697                 input: {
698                     "input_bool": true,
699                 }
700             },
701             {
702                 definition: {
703                     "id": "#main/input_int",
704                     "type": "int"
705                 },
706                 input: {
707                     "input_int": 1,
708                 }
709             },
710             {
711                 definition: {
712                     "id": "#main/input_long",
713                     "type": "long"
714                 },
715                 input: {
716                     "input_long" : 1,
717                 }
718             },
719             {
720                 definition: {
721                     "id": "#main/input_float",
722                     "type": "float"
723                 },
724                 input: {
725                     "input_float": 1.5,
726                 }
727             },
728             {
729                 definition: {
730                     "id": "#main/input_double",
731                     "type": "double"
732                 },
733                 input: {
734                     "input_double": 1.3,
735                 }
736             },
737             {
738                 definition: {
739                     "id": "#main/input_string",
740                     "type": "string"
741                 },
742                 input: {
743                     "input_string": "Hello World",
744                 }
745             },
746             {
747                 definition: {
748                     "id": "#main/input_file_array",
749                     "type": {
750                       "items": "File",
751                       "type": "array"
752                     }
753                 },
754                 input: {
755                     "input_file_array": [
756                         {
757                             "basename": "input2.tar",
758                             "class": "File",
759                             "location": "keep:00000000000000000000000000000000+02/input2.tar"
760                         },
761                         {
762                             "basename": "input3.tar",
763                             "class": "File",
764                             "location": "keep:00000000000000000000000000000000+03/input3.tar",
765                             "secondaryFiles": [
766                                 {
767                                     "basename": "input3-2.txt",
768                                     "class": "File",
769                                     "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
770                                 }
771                             ]
772                         },
773                         {
774                             "$import": "import_path"
775                         }
776                     ]
777                 }
778             },
779             {
780                 definition: {
781                     "id": "#main/input_dir_array",
782                     "type": {
783                       "items": "Directory",
784                       "type": "array"
785                     }
786                 },
787                 input: {
788                     "input_dir_array": [
789                         {
790                             "basename": "11111111111111111111111111111111+02",
791                             "class": "Directory",
792                             "location": "keep:11111111111111111111111111111111+02"
793                         },
794                         {
795                             "basename": "11111111111111111111111111111111+03",
796                             "class": "Directory",
797                             "location": "keep:11111111111111111111111111111111+03"
798                         },
799                         {
800                             "$import": "import_path"
801                         }
802                     ]
803                 }
804             },
805             {
806                 definition: {
807                     "id": "#main/input_int_array",
808                     "type": {
809                       "items": "int",
810                       "type": "array"
811                     }
812                 },
813                 input: {
814                     "input_int_array": [
815                         1,
816                         3,
817                         5,
818                         {
819                             "$import": "import_path"
820                         }
821                     ]
822                 }
823             },
824             {
825                 definition: {
826                     "id": "#main/input_long_array",
827                     "type": {
828                       "items": "long",
829                       "type": "array"
830                     }
831                 },
832                 input: {
833                     "input_long_array": [
834                         10,
835                         20,
836                         {
837                             "$import": "import_path"
838                         }
839                     ]
840                 }
841             },
842             {
843                 definition: {
844                     "id": "#main/input_float_array",
845                     "type": {
846                       "items": "float",
847                       "type": "array"
848                     }
849                 },
850                 input: {
851                     "input_float_array": [
852                         10.2,
853                         10.4,
854                         10.6,
855                         {
856                             "$import": "import_path"
857                         }
858                     ]
859                 }
860             },
861             {
862                 definition: {
863                     "id": "#main/input_double_array",
864                     "type": {
865                       "items": "double",
866                       "type": "array"
867                     }
868                 },
869                 input: {
870                     "input_double_array": [
871                         20.1,
872                         20.2,
873                         20.3,
874                         {
875                             "$import": "import_path"
876                         }
877                     ]
878                 }
879             },
880             {
881                 definition: {
882                     "id": "#main/input_string_array",
883                     "type": {
884                       "items": "string",
885                       "type": "array"
886                     }
887                 },
888                 input: {
889                     "input_string_array": [
890                         "Hello",
891                         "World",
892                         "!",
893                         {
894                             "$import": "import_path"
895                         }
896                     ]
897                 }
898             },
899             {
900                 definition: {
901                     "id": "#main/input_bool_include",
902                     "type": "boolean"
903                 },
904                 input: {
905                     "input_bool_include": {
906                         "$include": "include_path"
907                     }
908                 }
909             },
910             {
911                 definition: {
912                     "id": "#main/input_int_include",
913                     "type": "int"
914                 },
915                 input: {
916                     "input_int_include": {
917                         "$include": "include_path"
918                     }
919                 }
920             },
921             {
922                 definition: {
923                     "id": "#main/input_float_include",
924                     "type": "float"
925                 },
926                 input: {
927                     "input_float_include": {
928                         "$include": "include_path"
929                     }
930                 }
931             },
932             {
933                 definition: {
934                     "id": "#main/input_string_include",
935                     "type": "string"
936                 },
937                 input: {
938                     "input_string_include": {
939                         "$include": "include_path"
940                     }
941                 }
942             },
943             {
944                 definition: {
945                     "id": "#main/input_file_include",
946                     "type": "File"
947                 },
948                 input: {
949                     "input_file_include": {
950                         "$include": "include_path"
951                     }
952                 }
953             },
954             {
955                 definition: {
956                     "id": "#main/input_directory_include",
957                     "type": "Directory"
958                 },
959                 input: {
960                     "input_directory_include": {
961                         "$include": "include_path"
962                     }
963                 }
964             },
965             {
966                 definition: {
967                     "id": "#main/input_file_url",
968                     "type": "File"
969                 },
970                 input: {
971                     "input_file_url": {
972                         "basename": "index.html",
973                         "class": "File",
974                         "location": "http://example.com/index.html"
975                       }
976                 }
977             }
978         ];
979
980         const testOutputs = [
981             {
982                 definition: {
983                     "id": "#main/output_file",
984                     "label": "Label Description",
985                     "type": "File"
986                 },
987                 output: {
988                     "output_file": {
989                         "basename": "cat.png",
990                         "class": "File",
991                         "location": "cat.png"
992                     }
993                 }
994             },
995             {
996                 definition: {
997                     "id": "#main/output_file_with_secondary",
998                     "doc": "Doc Description",
999                     "type": "File"
1000                 },
1001                 output: {
1002                     "output_file_with_secondary": {
1003                         "basename": "main.dat",
1004                         "class": "File",
1005                         "location": "main.dat",
1006                         "secondaryFiles": [
1007                             {
1008                                 "basename": "secondary.dat",
1009                                 "class": "File",
1010                                 "location": "secondary.dat"
1011                             },
1012                             {
1013                                 "basename": "secondary2.dat",
1014                                 "class": "File",
1015                                 "location": "secondary2.dat"
1016                             }
1017                         ]
1018                     }
1019                 }
1020             },
1021             {
1022                 definition: {
1023                     "id": "#main/output_dir",
1024                     "doc": ["Doc desc 1", "Doc desc 2"],
1025                     "type": "Directory"
1026                 },
1027                 output: {
1028                     "output_dir": {
1029                         "basename": "outdir1",
1030                         "class": "Directory",
1031                         "location": "outdir1"
1032                     }
1033                 }
1034             },
1035             {
1036                 definition: {
1037                     "id": "#main/output_bool",
1038                     "type": "boolean"
1039                 },
1040                 output: {
1041                     "output_bool": true
1042                 }
1043             },
1044             {
1045                 definition: {
1046                     "id": "#main/output_int",
1047                     "type": "int"
1048                 },
1049                 output: {
1050                     "output_int": 1
1051                 }
1052             },
1053             {
1054                 definition: {
1055                     "id": "#main/output_long",
1056                     "type": "long"
1057                 },
1058                 output: {
1059                     "output_long": 1
1060                 }
1061             },
1062             {
1063                 definition: {
1064                     "id": "#main/output_float",
1065                     "type": "float"
1066                 },
1067                 output: {
1068                     "output_float": 100.5
1069                 }
1070             },
1071             {
1072                 definition: {
1073                     "id": "#main/output_double",
1074                     "type": "double"
1075                 },
1076                 output: {
1077                     "output_double": 100.3
1078                 }
1079             },
1080             {
1081                 definition: {
1082                     "id": "#main/output_string",
1083                     "type": "string"
1084                 },
1085                 output: {
1086                     "output_string": "Hello output"
1087                 }
1088             },
1089             {
1090                 definition: {
1091                     "id": "#main/output_file_array",
1092                     "type": {
1093                         "items": "File",
1094                         "type": "array"
1095                     }
1096                 },
1097                 output: {
1098                     "output_file_array": [
1099                         {
1100                             "basename": "output2.tar",
1101                             "class": "File",
1102                             "location": "output2.tar"
1103                         },
1104                         {
1105                             "basename": "output3.tar",
1106                             "class": "File",
1107                             "location": "output3.tar"
1108                         }
1109                     ]
1110                 }
1111             },
1112             {
1113                 definition: {
1114                     "id": "#main/output_dir_array",
1115                     "type": {
1116                         "items": "Directory",
1117                         "type": "array"
1118                     }
1119                 },
1120                 output: {
1121                     "output_dir_array": [
1122                         {
1123                             "basename": "outdir2",
1124                             "class": "Directory",
1125                             "location": "outdir2"
1126                         },
1127                         {
1128                             "basename": "outdir3",
1129                             "class": "Directory",
1130                             "location": "outdir3"
1131                         }
1132                     ]
1133                 }
1134             },
1135             {
1136                 definition: {
1137                     "id": "#main/output_int_array",
1138                     "type": {
1139                         "items": "int",
1140                         "type": "array"
1141                     }
1142                 },
1143                 output: {
1144                     "output_int_array": [
1145                         10,
1146                         11,
1147                         12
1148                     ]
1149                 }
1150             },
1151             {
1152                 definition: {
1153                     "id": "#main/output_long_array",
1154                     "type": {
1155                         "items": "long",
1156                         "type": "array"
1157                     }
1158                 },
1159                 output: {
1160                     "output_long_array": [
1161                         51,
1162                         52
1163                     ]
1164                 }
1165             },
1166             {
1167                 definition: {
1168                     "id": "#main/output_float_array",
1169                     "type": {
1170                         "items": "float",
1171                         "type": "array"
1172                     }
1173                 },
1174                 output: {
1175                     "output_float_array": [
1176                         100.2,
1177                         100.4,
1178                         100.6
1179                     ]
1180                 }
1181             },
1182             {
1183                 definition: {
1184                     "id": "#main/output_double_array",
1185                     "type": {
1186                         "items": "double",
1187                         "type": "array"
1188                     }
1189                 },
1190                 output: {
1191                     "output_double_array": [
1192                         100.1,
1193                         100.2,
1194                         100.3
1195                     ]
1196                 }
1197             },
1198             {
1199                 definition: {
1200                     "id": "#main/output_string_array",
1201                     "type": {
1202                         "items": "string",
1203                         "type": "array"
1204                     }
1205                 },
1206                 output: {
1207                     "output_string_array": [
1208                         "Hello",
1209                         "Output",
1210                         "!"
1211                     ]
1212                 }
1213             }
1214         ];
1215
1216         const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1217             cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
1218                 label && cy.contains(label);
1219
1220                 if (multipleRows) {
1221                     cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
1222                     if (val) {
1223                         if (Array.isArray(val)) {
1224                             val.forEach(v => cy.get('@secondaryRows').contains(v));
1225                         } else {
1226                             cy.get('@secondaryRows').contains(val);
1227                         }
1228                     }
1229                     if (collection) {
1230                         cy.get('@secondaryRows').contains(collection);
1231                     }
1232                 } else {
1233                     if (val) {
1234                         if (Array.isArray(val)) {
1235                             val.forEach(v => cy.contains(v));
1236                         } else {
1237                             cy.contains(val);
1238                         }
1239                     }
1240                     if (collection) {
1241                         cy.contains(collection);
1242                     }
1243                 }
1244
1245
1246             });
1247         };
1248
1249         const verifyIOParameterImage = (name, url) => {
1250             cy.get('table tr').contains(name).parents('tr').within(() => {
1251                 cy.get('[alt="Inline Preview"]')
1252                     .should('be.visible')
1253                     .and(($img) => {
1254                         expect($img[0].naturalWidth).to.be.greaterThan(0);
1255                         expect($img[0].src).contains(url);
1256                     })
1257             });
1258         };
1259
1260         it('displays IO parameters with keep links and previews', function() {
1261             // Create output collection for real files
1262             cy.createCollection(adminUser.token, {
1263                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1264                 owner_uuid: activeUser.user.uuid,
1265             }).then((testOutputCollection) => {
1266                         cy.loginAs(activeUser);
1267
1268                         cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1269
1270                         cy.get('[data-cy=upload-button]').click();
1271
1272                         cy.fixture('files/cat.png', 'base64').then(content => {
1273                             cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
1274                             cy.get('[data-cy=form-submit-btn]').click();
1275                             cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
1276                             // Confirm final collection state.
1277                             cy.get('[data-cy=collection-files-panel]')
1278                                 .contains('cat.png').should('exist');
1279                         });
1280
1281                         cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
1282                     });
1283
1284             // Get updated collection pdh
1285             cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
1286                 // Add output uuid and inputs to container request
1287                 cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1288                     req.reply((res) => {
1289                         res.body.output_uuid = testOutputCollection.uuid;
1290                         res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1291                             content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
1292                         };
1293                         res.body.mounts["/var/lib/cwl/workflow.json"] = {
1294                             content: {
1295                                 $graph: [{
1296                                     id: "#main",
1297                                     inputs: testInputs.map((input) => (input.definition)),
1298                                     outputs: testOutputs.map((output) => (output.definition))
1299                                 }]
1300                             }
1301                         };
1302                     });
1303                 });
1304
1305                 // Stub fake output collection
1306                 cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
1307                     statusCode: 200,
1308                     body: {
1309                         uuid: testOutputCollection.uuid,
1310                         portable_data_hash: testOutputCollection.portable_data_hash,
1311                     }
1312                 });
1313
1314                 // Stub fake output json
1315                 cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
1316                     statusCode: 200,
1317                     body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
1318                 });
1319
1320                 // Stub webdav response, points to output json
1321                 cy.intercept({method: 'PROPFIND', url: '*'}, {
1322                     fixture: 'webdav-propfind-outputs.xml',
1323                 });
1324             });
1325
1326             createContainerRequest(
1327                 activeUser,
1328                 'test_container_request',
1329                 'arvados/jobs',
1330                 ['echo', 'hello world'],
1331                 false, 'Committed')
1332             .as('containerRequest');
1333
1334             cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
1335                 cy.goToPath(`/processes/${containerRequest.uuid}`);
1336                 cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1337                     .parents('[data-cy=process-io-card]').within(() => {
1338                         verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
1339                         verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
1340                         verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
1341                         verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
1342                         verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
1343                         verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
1344                         verifyIOParameter('input_int', null, null, '1');
1345                         verifyIOParameter('input_long', null, null, '1');
1346                         verifyIOParameter('input_float', null, null, '1.5');
1347                         verifyIOParameter('input_double', null, null, '1.3');
1348                         verifyIOParameter('input_string', null, null, 'Hello World');
1349                         verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
1350                         verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
1351                         verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
1352                         verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
1353                         verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
1354                         verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
1355                         verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
1356                         verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
1357                         verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
1358                         verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1359                         verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1360                         verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
1361                         verifyIOParameter('input_bool_include', null, null, "Cannot display value");
1362                         verifyIOParameter('input_int_include', null, null, "Cannot display value");
1363                         verifyIOParameter('input_float_include', null, null, "Cannot display value");
1364                         verifyIOParameter('input_string_include', null, null, "Cannot display value");
1365                         verifyIOParameter('input_file_include', null, null, "Cannot display value");
1366                         verifyIOParameter('input_directory_include', null, null, "Cannot display value");
1367                         verifyIOParameter('input_file_url', null, null, "http://example.com/index.html");
1368                     });
1369                 cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1370                     .parents('[data-cy=process-io-card]').within((ctx) => {
1371                         cy.get(ctx).scrollIntoView();
1372                         cy.get('[data-cy="io-preview-image-toggle"]').click({waitForAnimations: false});
1373                         const outPdh = testOutputCollection.portable_data_hash;
1374
1375                         verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
1376                         verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
1377                         verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
1378                         verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
1379                         verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
1380                         verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
1381                         verifyIOParameter('output_bool', null, null, 'true');
1382                         verifyIOParameter('output_int', null, null, '1');
1383                         verifyIOParameter('output_long', null, null, '1');
1384                         verifyIOParameter('output_float', null, null, '100.5');
1385                         verifyIOParameter('output_double', null, null, '100.3');
1386                         verifyIOParameter('output_string', null, null, 'Hello output');
1387                         verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
1388                         verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
1389                         verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
1390                         verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
1391                         verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
1392                         verifyIOParameter('output_long_array', null, null, ["51", "52"]);
1393                         verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
1394                         verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
1395                         verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
1396                     });
1397             });
1398         });
1399
1400         it('displays IO parameters with no value', function() {
1401
1402             const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
1403             const fakeOutputPDH = '11111111111111111111111111111111+99/';
1404
1405             cy.loginAs(activeUser);
1406
1407             // Add output uuid and inputs to container request
1408             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1409                 req.reply((res) => {
1410                     res.body.output_uuid = fakeOutputUUID;
1411                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1412                         content: {}
1413                     };
1414                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
1415                         content: {
1416                             $graph: [{
1417                                 id: "#main",
1418                                 inputs: testInputs.map((input) => (input.definition)),
1419                                 outputs: testOutputs.map((output) => (output.definition))
1420                             }]
1421                         }
1422                     };
1423                 });
1424             });
1425
1426             // Stub fake output collection
1427             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
1428                 statusCode: 200,
1429                 body: {
1430                     uuid: fakeOutputUUID,
1431                     portable_data_hash: fakeOutputPDH,
1432                 }
1433             });
1434
1435             // Stub fake output json
1436             cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
1437                 statusCode: 200,
1438                 body: {}
1439             });
1440
1441             cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
1442                 // Stub webdav response, points to output json
1443                 cy.intercept({method: 'PROPFIND', url: '*'}, {
1444                     statusCode: 200,
1445                     body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
1446                 });
1447             });
1448
1449             createContainerRequest(
1450                 activeUser,
1451                 'test_container_request',
1452                 'arvados/jobs',
1453                 ['echo', 'hello world'],
1454                 false, 'Committed')
1455             .as('containerRequest');
1456
1457             cy.getAll('@containerRequest').then(function([containerRequest]) {
1458                 cy.goToPath(`/processes/${containerRequest.uuid}`);
1459                 cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1460                     .parents('[data-cy=process-io-card]').within(() => {
1461                         cy.wait(2000);
1462                         cy.waitForDom();
1463                         cy.get('tbody tr').each((item) => {
1464                             cy.wrap(item).contains('No value');
1465                         });
1466                     });
1467                 cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1468                     .parents('[data-cy=process-io-card]').within(() => {
1469                         cy.get('tbody tr').each((item) => {
1470                             cy.wrap(item).contains('No value');
1471                         });
1472                     });
1473             });
1474         });
1475     });
1476
1477 });