16073: Add better empty value handling for process io panels
[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     it('shows process logs', function() {
88         const crName = 'test_container_request';
89         createContainerRequest(
90             activeUser,
91             crName,
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', crName);
99             cy.get('[data-cy=process-logs]')
100                 .should('contain', 'No logs yet')
101                 .and('not.contain', 'hello world');
102             cy.createLog(activeUser.token, {
103                 object_uuid: containerRequest.container_uuid,
104                 properties: {
105                     text: 'hello world'
106                 },
107                 event_type: 'stdout'
108             }).then(function(log) {
109                 cy.get('[data-cy=process-logs]')
110                     .should('not.contain', 'No logs yet')
111                     .and('contain', 'hello world');
112             })
113         });
114     });
115
116     it('filters process logs by event type', function() {
117         const nodeInfoLogs = [
118             'Host Information',
119             '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',
120             'CPU Information',
121             'processor  : 0',
122             'vendor_id  : GenuineIntel',
123             'cpu family : 6',
124             'model      : 79',
125             'model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz'
126         ];
127         const crunchRunLogs = [
128             '2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection',
129             '2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started',
130             '2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)',
131             '2022-03-22T13:56:26.244862836Z Executing container \'zzzzz-dz642-1wokwvcct9s9du3\' using docker runtime',
132             '2022-03-22T13:56:26.245037738Z Executing on host \'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p\'',
133         ];
134         const stdoutLogs = [
135             'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.',
136             'Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.',
137             'In hac habitasse platea dictumst.',
138             'Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.',
139             'Interdum et malesuada fames ac ante ipsum primis in faucibus.',
140             'Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.',
141             'Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.',
142             'Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.',
143             'Donec vitae leo id augue gravida bibendum.',
144             'Nam libero libero, pretium ac faucibus elementum, mattis nec ex.',
145             'Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.',
146             'Aliquam viverra nisi nulla, et efficitur dolor mattis in.',
147             '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.',
148             '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.',
149             'Phasellus non ex quis arcu tempus faucibus molestie in sapien.',
150             'Duis tristique semper dolor, vitae pulvinar risus.',
151             'Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.',
152             'Nulla eget mollis ipsum.',
153         ];
154
155         createContainerRequest(
156             activeUser,
157             'test_container_request',
158             'arvados/jobs',
159             ['echo', 'hello world'],
160             false, 'Committed')
161         .then(function(containerRequest) {
162             cy.logsForContainer(activeUser.token, containerRequest.container_uuid,
163                 'node-info', nodeInfoLogs).as('nodeInfoLogs');
164             cy.logsForContainer(activeUser.token, containerRequest.container_uuid,
165                 'crunch-run', crunchRunLogs).as('crunchRunLogs');
166             cy.logsForContainer(activeUser.token, containerRequest.container_uuid,
167                 'stdout', stdoutLogs).as('stdoutLogs');
168             cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
169                 cy.loginAs(activeUser);
170                 cy.goToPath(`/processes/${containerRequest.uuid}`);
171                 // Should show main logs by default
172                 cy.get('[data-cy=process-logs-filter]').should('contain', 'Main logs');
173                 cy.get('[data-cy=process-logs]')
174                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
175                     .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
176                     .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
177                 // Select 'All logs'
178                 cy.get('[data-cy=process-logs-filter]').click();
179                 cy.get('body').contains('li', 'All logs').click();
180                 cy.get('[data-cy=process-logs]')
181                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
182                     .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
183                     .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
184                 // Select 'node-info' logs
185                 cy.get('[data-cy=process-logs-filter]').click();
186                 cy.get('body').contains('li', 'node-info').click();
187                 cy.get('[data-cy=process-logs]')
188                     .should('not.contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
189                     .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
190                     .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
191                 // Select 'stdout' logs
192                 cy.get('[data-cy=process-logs-filter]').click();
193                 cy.get('body').contains('li', 'stdout').click();
194                 cy.get('[data-cy=process-logs]')
195                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
196                     .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
197                     .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
198             });
199         });
200     });
201
202     it('should show runtime status indicators', function() {
203         // Setup running container with runtime_status error & warning messages
204         createContainerRequest(
205             activeUser,
206             'test_container_request',
207             'arvados/jobs',
208             ['echo', 'hello world'],
209             false, 'Committed')
210         .as('containerRequest')
211         .then(function(containerRequest) {
212             expect(containerRequest.state).to.equal('Committed');
213             expect(containerRequest.container_uuid).not.to.be.equal('');
214
215             cy.getContainer(activeUser.token, containerRequest.container_uuid)
216             .then(function(queuedContainer) {
217                 expect(queuedContainer.state).to.be.equal('Queued');
218             });
219             cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
220                 state: 'Locked'
221             }).then(function(lockedContainer) {
222                 expect(lockedContainer.state).to.be.equal('Locked');
223
224                 cy.updateContainer(adminUser.token, lockedContainer.uuid, {
225                     state: 'Running',
226                     runtime_status: {
227                         error: 'Something went wrong',
228                         errorDetail: 'Process exited with status 1',
229                         warning: 'Free disk space is low',
230                     }
231                 })
232                 .as('runningContainer')
233                 .then(function(runningContainer) {
234                     expect(runningContainer.state).to.be.equal('Running');
235                     expect(runningContainer.runtime_status).to.be.deep.equal({
236                         'error': 'Something went wrong',
237                         'errorDetail': 'Process exited with status 1',
238                         'warning': 'Free disk space is low',
239                     });
240                 });
241             })
242         });
243         // Test that the UI shows the error and warning messages
244         cy.getAll('@containerRequest', '@runningContainer').then(function([containerRequest]) {
245             cy.loginAs(activeUser);
246             cy.goToPath(`/processes/${containerRequest.uuid}`);
247             cy.get('[data-cy=process-runtime-status-error]')
248                 .should('contain', 'Something went wrong')
249                 .and('contain', 'Process exited with status 1');
250             cy.get('[data-cy=process-runtime-status-warning]')
251                 .should('contain', 'Free disk space is low')
252                 .and('contain', 'No additional warning details available');
253         });
254
255
256         // Force container_count for testing
257         let containerCount = 2;
258         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
259             req.reply((res) => {
260                 res.body.container_count = containerCount;
261             });
262         });
263
264         cy.getAll('@containerRequest').then(function([containerRequest]) {
265             cy.goToPath(`/processes/${containerRequest.uuid}`);
266             cy.get('[data-cy=process-runtime-status-retry-warning]')
267                 .should('contain', 'Process retried 1 time');
268         });
269
270         cy.getAll('@containerRequest').then(function([containerRequest]) {
271             containerCount = 3;
272             cy.goToPath(`/processes/${containerRequest.uuid}`);
273             cy.get('[data-cy=process-runtime-status-retry-warning]')
274                 .should('contain', 'Process retried 2 times');
275         });
276     });
277
278
279     const testInputs = [
280         {
281             definition: {
282                 "id": "#main/input_file",
283                 "label": "Label Description",
284                 "type": "File"
285             },
286             input: {
287                 "input_file": {
288                     "basename": "input1.tar",
289                     "class": "File",
290                     "location": "keep:00000000000000000000000000000000+01/input1.tar",
291                     "secondaryFiles": [
292                         {
293                             "basename": "input1-2.txt",
294                             "class": "File",
295                             "location": "keep:00000000000000000000000000000000+01/input1-2.txt"
296                         },
297                         {
298                             "basename": "input1-3.txt",
299                             "class": "File",
300                             "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
301                         },
302                         {
303                             "basename": "input1-4.txt",
304                             "class": "File",
305                             "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
306                         }
307                     ]
308                 }
309             }
310         },
311         {
312             definition: {
313                 "id": "#main/input_dir",
314                 "doc": "Doc Description",
315                 "type": "Directory"
316             },
317             input: {
318                 "input_dir": {
319                     "basename": "11111111111111111111111111111111+01",
320                     "class": "Directory",
321                     "location": "keep:11111111111111111111111111111111+01"
322                 }
323             }
324         },
325         {
326             definition: {
327                 "id": "#main/input_bool",
328                 "doc": ["Doc desc 1", "Doc desc 2"],
329                 "type": "boolean"
330             },
331             input: {
332                 "input_bool": true,
333             }
334         },
335         {
336             definition: {
337                 "id": "#main/input_int",
338                 "type": "int"
339             },
340             input: {
341                 "input_int": 1,
342             }
343         },
344         {
345             definition: {
346                 "id": "#main/input_long",
347                 "type": "long"
348             },
349             input: {
350                 "input_long" : 1,
351             }
352         },
353         {
354             definition: {
355                 "id": "#main/input_float",
356                 "type": "float"
357             },
358             input: {
359                 "input_float": 1.5,
360             }
361         },
362         {
363             definition: {
364                 "id": "#main/input_double",
365                 "type": "double"
366             },
367             input: {
368                 "input_double": 1.3,
369             }
370         },
371         {
372             definition: {
373                 "id": "#main/input_string",
374                 "type": "string"
375             },
376             input: {
377                 "input_string": "Hello World",
378             }
379         },
380         {
381             definition: {
382                 "id": "#main/input_file_array",
383                 "type": {
384                   "items": "File",
385                   "type": "array"
386                 }
387             },
388             input: {
389                 "input_file_array": [
390                     {
391                         "basename": "input2.tar",
392                         "class": "File",
393                         "location": "keep:00000000000000000000000000000000+02/input2.tar"
394                     },
395                     {
396                         "basename": "input3.tar",
397                         "class": "File",
398                         "location": "keep:00000000000000000000000000000000+03/input3.tar",
399                         "secondaryFiles": [
400                             {
401                                 "basename": "input3-2.txt",
402                                 "class": "File",
403                                 "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
404                             }
405                         ]
406                     }
407                 ]
408             }
409         },
410         {
411             definition: {
412                 "id": "#main/input_dir_array",
413                 "type": {
414                   "items": "Directory",
415                   "type": "array"
416                 }
417             },
418             input: {
419                 "input_dir_array": [
420                     {
421                         "basename": "11111111111111111111111111111111+02",
422                         "class": "Directory",
423                         "location": "keep:11111111111111111111111111111111+02"
424                     },
425                     {
426                         "basename": "11111111111111111111111111111111+03",
427                         "class": "Directory",
428                         "location": "keep:11111111111111111111111111111111+03"
429                     }
430                 ]
431             }
432         },
433         {
434             definition: {
435                 "id": "#main/input_int_array",
436                 "type": {
437                   "items": "int",
438                   "type": "array"
439                 }
440             },
441             input: {
442                 "input_int_array": [
443                     1,
444                     3,
445                     5
446                 ]
447             }
448         },
449         {
450             definition: {
451                 "id": "#main/input_long_array",
452                 "type": {
453                   "items": "long",
454                   "type": "array"
455                 }
456             },
457             input: {
458                 "input_long_array": [
459                     10,
460                     20
461                 ]
462             }
463         },
464         {
465             definition: {
466                 "id": "#main/input_float_array",
467                 "type": {
468                   "items": "float",
469                   "type": "array"
470                 }
471             },
472             input: {
473                 "input_float_array": [
474                     10.2,
475                     10.4,
476                     10.6
477                 ]
478             }
479         },
480         {
481             definition: {
482                 "id": "#main/input_double_array",
483                 "type": {
484                   "items": "double",
485                   "type": "array"
486                 }
487             },
488             input: {
489                 "input_double_array": [
490                     20.1,
491                     20.2,
492                     20.3
493                 ]
494             }
495         },
496         {
497             definition: {
498                 "id": "#main/input_string_array",
499                 "type": {
500                   "items": "string",
501                   "type": "array"
502                 }
503             },
504             input: {
505                 "input_string_array": [
506                     "Hello",
507                     "World",
508                     "!"
509                 ]
510             }
511         }
512     ];
513
514     const testOutputs = [
515         {
516             definition: {
517                 "id": "#main/output_file",
518                 "label": "Label Description",
519                 "type": "File"
520             },
521             output: {
522                 "output_file": {
523                     "basename": "cat.png",
524                     "class": "File",
525                     "location": "cat.png"
526                 }
527             }
528         },
529         {
530             definition: {
531                 "id": "#main/output_file_with_secondary",
532                 "doc": "Doc Description",
533                 "type": "File"
534             },
535             output: {
536                 "output_file_with_secondary": {
537                     "basename": "main.dat",
538                     "class": "File",
539                     "location": "main.dat",
540                     "secondaryFiles": [
541                         {
542                             "basename": "secondary.dat",
543                             "class": "File",
544                             "location": "secondary.dat"
545                         },
546                         {
547                             "basename": "secondary2.dat",
548                             "class": "File",
549                             "location": "secondary2.dat"
550                         }
551                     ]
552                 }
553             }
554         },
555         {
556             definition: {
557                 "id": "#main/output_dir",
558                 "doc": ["Doc desc 1", "Doc desc 2"],
559                 "type": "Directory"
560             },
561             output: {
562                 "output_dir": {
563                     "basename": "outdir1",
564                     "class": "Directory",
565                     "location": "outdir1"
566                 }
567             }
568         },
569         {
570             definition: {
571                 "id": "#main/output_bool",
572                 "type": "boolean"
573             },
574             output: {
575                 "output_bool": true
576             }
577         },
578         {
579             definition: {
580                 "id": "#main/output_int",
581                 "type": "int"
582             },
583             output: {
584                 "output_int": 1
585             }
586         },
587         {
588             definition: {
589                 "id": "#main/output_long",
590                 "type": "long"
591             },
592             output: {
593                 "output_long": 1
594             }
595         },
596         {
597             definition: {
598                 "id": "#main/output_float",
599                 "type": "float"
600             },
601             output: {
602                 "output_float": 100.5
603             }
604         },
605         {
606             definition: {
607                 "id": "#main/output_double",
608                 "type": "double"
609             },
610             output: {
611                 "output_double": 100.3
612             }
613         },
614         {
615             definition: {
616                 "id": "#main/output_string",
617                 "type": "string"
618             },
619             output: {
620                 "output_string": "Hello output"
621             }
622         },
623         {
624             definition: {
625                 "id": "#main/output_file_array",
626                 "type": {
627                     "items": "File",
628                     "type": "array"
629                 }
630             },
631             output: {
632                 "output_file_array": [
633                     {
634                         "basename": "output2.tar",
635                         "class": "File",
636                         "location": "output2.tar"
637                     },
638                     {
639                         "basename": "output3.tar",
640                         "class": "File",
641                         "location": "output3.tar"
642                     }
643                 ]
644             }
645         },
646         {
647             definition: {
648                 "id": "#main/output_dir_array",
649                 "type": {
650                     "items": "Directory",
651                     "type": "array"
652                 }
653             },
654             output: {
655                 "output_dir_array": [
656                     {
657                         "basename": "outdir2",
658                         "class": "Directory",
659                         "location": "outdir2"
660                     },
661                     {
662                         "basename": "outdir3",
663                         "class": "Directory",
664                         "location": "outdir3"
665                     }
666                 ]
667             }
668         },
669         {
670             definition: {
671                 "id": "#main/output_int_array",
672                 "type": {
673                     "items": "int",
674                     "type": "array"
675                 }
676             },
677             output: {
678                 "output_int_array": [
679                     10,
680                     11,
681                     12
682                 ]
683             }
684         },
685         {
686             definition: {
687                 "id": "#main/output_long_array",
688                 "type": {
689                     "items": "long",
690                     "type": "array"
691                 }
692             },
693             output: {
694                 "output_long_array": [
695                     51,
696                     52
697                 ]
698             }
699         },
700         {
701             definition: {
702                 "id": "#main/output_float_array",
703                 "type": {
704                     "items": "float",
705                     "type": "array"
706                 }
707             },
708             output: {
709                 "output_float_array": [
710                     100.2,
711                     100.4,
712                     100.6
713                 ]
714             }
715         },
716         {
717             definition: {
718                 "id": "#main/output_double_array",
719                 "type": {
720                     "items": "double",
721                     "type": "array"
722                 }
723             },
724             output: {
725                 "output_double_array": [
726                     100.1,
727                     100.2,
728                     100.3
729                 ]
730             }
731         },
732         {
733             definition: {
734                 "id": "#main/output_string_array",
735                 "type": {
736                     "items": "string",
737                     "type": "array"
738                 }
739             },
740             output: {
741                 "output_string_array": [
742                     "Hello",
743                     "Output",
744                     "!"
745                 ]
746             }
747         }
748     ];
749
750     const verifyIOParameter = (name, doc, val) => {
751         cy.get('table tr').contains(name).parents('tr').within(() => {
752             doc && cy.contains(doc);
753             if (val) {
754                 if (Array.isArray(val)) {
755                     val.forEach(v => cy.contains(v));
756                 } else {
757                     cy.contains(val);
758                 }
759             }
760         });
761     };
762
763     const verifyIOParameterImage = (name, url) => {
764         cy.get('table tr').contains(name).parents('tr').within(() => {
765             cy.get('[alt="Inline Preview"]')
766                 .should('be.visible')
767                 .and(($img) => {
768                     expect($img[0].naturalWidth).to.be.greaterThan(0);
769                     expect($img[0].src).contains(url);
770                 })
771         });
772     };
773
774     it('displays IO parameters with keep links and previews', function() {
775         // Create output collection for real files
776         cy.createCollection(adminUser.token, {
777             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
778             owner_uuid: activeUser.user.uuid,
779         }).then((testOutputCollection) => {
780                     cy.loginAs(activeUser);
781
782                     cy.goToPath(`/collections/${testOutputCollection.uuid}`);
783
784                     cy.get('[data-cy=upload-button]').click();
785
786                     cy.fixture('files/cat.png', 'base64').then(content => {
787                         cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
788                         cy.get('[data-cy=form-submit-btn]').click();
789                         cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
790                         // Confirm final collection state.
791                         cy.get('[data-cy=collection-files-panel]')
792                             .contains('cat.png').should('exist');
793                     });
794
795                     cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
796                 });
797
798         // Get updated collection pdh
799         cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
800             // Add output uuid and inputs to container request
801             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
802                 req.reply((res) => {
803                     res.body.output_uuid = testOutputCollection.uuid;
804                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
805                         content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
806                     };
807                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
808                         content: {
809                             $graph: [{
810                                 id: "#main",
811                                 inputs: testInputs.map((input) => (input.definition)),
812                                 outputs: testOutputs.map((output) => (output.definition))
813                             }]
814                         }
815                     };
816                 });
817             });
818
819             // Stub fake output collection
820             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
821                 statusCode: 200,
822                 body: {
823                     uuid: testOutputCollection.uuid,
824                     portable_data_hash: testOutputCollection.portable_data_hash,
825                 }
826             });
827
828             // Stub fake output json
829             cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
830                 statusCode: 200,
831                 body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
832             });
833
834             // Stub webdav response, points to output json
835             cy.intercept({method: 'PROPFIND', url: '*'}, {
836                 fixture: 'webdav-propfind-outputs.xml',
837             });
838         });
839
840         createContainerRequest(
841             activeUser,
842             'test_container_request',
843             'arvados/jobs',
844             ['echo', 'hello world'],
845             false, 'Committed')
846         .as('containerRequest');
847
848         cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
849             cy.goToPath(`/processes/${containerRequest.uuid}`);
850             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
851                 .parents('[data-cy=process-io-card]').within(() => {
852                     cy.wait(2000);
853                     cy.waitForDom();
854                     verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1.tar');
855                     verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1-2.txt');
856                     verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1-3.txt');
857                     verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1-4.txt');
858                     verifyIOParameter('input_dir', "Doc Description", 'keep:11111111111111111111111111111111+01/');
859                     verifyIOParameter('input_bool', "Doc desc 1, Doc desc 2", 'true');
860                     verifyIOParameter('input_int', null, '1');
861                     verifyIOParameter('input_long', null, '1');
862                     verifyIOParameter('input_float', null, '1.5');
863                     verifyIOParameter('input_double', null, '1.3');
864                     verifyIOParameter('input_string', null, 'Hello World');
865                     verifyIOParameter('input_file_array', null, 'keep:00000000000000000000000000000000+02/input2.tar');
866                     verifyIOParameter('input_file_array', null, 'keep:00000000000000000000000000000000+03/input3.tar');
867                     verifyIOParameter('input_file_array', null, 'keep:00000000000000000000000000000000+03/input3-2.txt');
868                     verifyIOParameter('input_dir_array', null, 'keep:11111111111111111111111111111111+02/');
869                     verifyIOParameter('input_dir_array', null, 'keep:11111111111111111111111111111111+03/');
870                     verifyIOParameter('input_int_array', null, ["1", "3", "5"]);
871                     verifyIOParameter('input_long_array', null, ["10", "20"]);
872                     verifyIOParameter('input_float_array', null, ["10.2", "10.4", "10.6"]);
873                     verifyIOParameter('input_double_array', null, ["20.1", "20.2", "20.3"]);
874                     verifyIOParameter('input_string_array', null, ["Hello", "World", "!"]);
875                 });
876             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
877                 .parents('[data-cy=process-io-card]').within((ctx) => {
878                     cy.get(ctx).scrollIntoView();
879                     const outPdh = testOutputCollection.portable_data_hash;
880
881                     verifyIOParameter('output_file', "Label Description", `keep:${outPdh}/cat.png`);
882                     verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
883                     verifyIOParameter('output_file_with_secondary', "Doc Description", `keep:${outPdh}/main.dat`);
884                     verifyIOParameter('output_file_with_secondary', "Doc Description", `keep:${outPdh}/secondary.dat`);
885                     verifyIOParameter('output_file_with_secondary', "Doc Description", `keep:${outPdh}/secondary2.dat`);
886                     verifyIOParameter('output_dir', "Doc desc 1, Doc desc 2", `keep:${outPdh}/outdir1`);
887                     verifyIOParameter('output_bool', null, 'true');
888                     verifyIOParameter('output_int', null, '1');
889                     verifyIOParameter('output_long', null, '1');
890                     verifyIOParameter('output_float', null, '100.5');
891                     verifyIOParameter('output_double', null, '100.3');
892                     verifyIOParameter('output_string', null, 'Hello output');
893                     verifyIOParameter('output_file_array', null, `keep:${outPdh}/output2.tar`);
894                     verifyIOParameter('output_file_array', null, `keep:${outPdh}/output3.tar`);
895                     verifyIOParameter('output_dir_array', null, `keep:${outPdh}/outdir2`);
896                     verifyIOParameter('output_dir_array', null, `keep:${outPdh}/outdir3`);
897                     verifyIOParameter('output_int_array', null, ["10", "11", "12"]);
898                     verifyIOParameter('output_long_array', null, ["51", "52"]);
899                     verifyIOParameter('output_float_array', null, ["100.2", "100.4", "100.6"]);
900                     verifyIOParameter('output_double_array', null, ["100.1", "100.2", "100.3"]);
901                     verifyIOParameter('output_string_array', null, ["Hello", "Output", "!"]);
902                 });
903         });
904     });
905
906     it('displays IO parameters with no value', function() {
907
908         const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
909         const fakeOutputPDH = '11111111111111111111111111111111+99/';
910
911         cy.loginAs(activeUser);
912
913         // Add output uuid and inputs to container request
914         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
915             req.reply((res) => {
916                 res.body.output_uuid = fakeOutputUUID;
917                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
918                     content: {}
919                 };
920                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
921                     content: {
922                         $graph: [{
923                             id: "#main",
924                             inputs: testInputs.map((input) => (input.definition)),
925                             outputs: testOutputs.map((output) => (output.definition))
926                         }]
927                     }
928                 };
929             });
930         });
931
932         // Stub fake output collection
933         cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
934             statusCode: 200,
935             body: {
936                 uuid: fakeOutputUUID,
937                 portable_data_hash: fakeOutputPDH,
938             }
939         });
940
941         // Stub fake output json
942         cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
943             statusCode: 200,
944             body: {}
945         });
946
947         cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
948             // Stub webdav response, points to output json
949             cy.intercept({method: 'PROPFIND', url: '*'}, {
950                 statusCode: 200,
951                 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
952             });
953         });
954
955         createContainerRequest(
956             activeUser,
957             'test_container_request',
958             'arvados/jobs',
959             ['echo', 'hello world'],
960             false, 'Committed')
961         .as('containerRequest');
962
963         cy.getAll('@containerRequest').then(function([containerRequest]) {
964             cy.goToPath(`/processes/${containerRequest.uuid}`);
965             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
966                 .parents('[data-cy=process-io-card]').within(() => {
967                     cy.wait(2000);
968                     cy.waitForDom();
969                     cy.get('tbody tr').each((item) => {
970                         cy.wrap(item).contains('No value');
971                     });
972                 });
973             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
974                 .parents('[data-cy=process-io-card]').within(() => {
975                     cy.get('tbody tr').each((item) => {
976                         cy.wrap(item).contains('No value');
977                     });
978                 });
979         });
980     });
981
982 });