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