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