b2942e4d33886865a6aac8e1d460f9070fff7c98
[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('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, label, doc, val, collection, multipleRows) => {
751         cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
752             doc && cy.contains(doc);
753
754             if (multipleRows) {
755                 cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
756                 if (val) {
757                     if (Array.isArray(val)) {
758                         val.forEach(v => cy.get('@secondaryRows').contains(v));
759                     } else {
760                         cy.get('@secondaryRows').contains(val);
761                     }
762                 }
763                 if (collection) {
764                     cy.get('@secondaryRows').contains(collection);
765                 }
766             } else {
767                 if (val) {
768                     if (Array.isArray(val)) {
769                         val.forEach(v => cy.contains(v));
770                     } else {
771                         cy.contains(val);
772                     }
773                 }
774                 if (collection) {
775                     cy.contains(collection);
776                 }
777             }
778
779
780         });
781     };
782
783     const verifyIOParameterImage = (name, url) => {
784         cy.get('table tr').contains(name).parents('tr').within(() => {
785             cy.get('[alt="Inline Preview"]')
786                 .should('be.visible')
787                 .and(($img) => {
788                     expect($img[0].naturalWidth).to.be.greaterThan(0);
789                     expect($img[0].src).contains(url);
790                 })
791         });
792     };
793
794     it('displays IO parameters with keep links and previews', function() {
795         // Create output collection for real files
796         cy.createCollection(adminUser.token, {
797             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
798             owner_uuid: activeUser.user.uuid,
799         }).then((testOutputCollection) => {
800                     cy.loginAs(activeUser);
801
802                     cy.goToPath(`/collections/${testOutputCollection.uuid}`);
803
804                     cy.get('[data-cy=upload-button]').click();
805
806                     cy.fixture('files/cat.png', 'base64').then(content => {
807                         cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
808                         cy.get('[data-cy=form-submit-btn]').click();
809                         cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
810                         // Confirm final collection state.
811                         cy.get('[data-cy=collection-files-panel]')
812                             .contains('cat.png').should('exist');
813                     });
814
815                     cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
816                 });
817
818         // Get updated collection pdh
819         cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
820             // Add output uuid and inputs to container request
821             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
822                 req.reply((res) => {
823                     res.body.output_uuid = testOutputCollection.uuid;
824                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
825                         content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
826                     };
827                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
828                         content: {
829                             $graph: [{
830                                 id: "#main",
831                                 inputs: testInputs.map((input) => (input.definition)),
832                                 outputs: testOutputs.map((output) => (output.definition))
833                             }]
834                         }
835                     };
836                 });
837             });
838
839             // Stub fake output collection
840             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
841                 statusCode: 200,
842                 body: {
843                     uuid: testOutputCollection.uuid,
844                     portable_data_hash: testOutputCollection.portable_data_hash,
845                 }
846             });
847
848             // Stub fake output json
849             cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
850                 statusCode: 200,
851                 body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
852             });
853
854             // Stub webdav response, points to output json
855             cy.intercept({method: 'PROPFIND', url: '*'}, {
856                 fixture: 'webdav-propfind-outputs.xml',
857             });
858         });
859
860         createContainerRequest(
861             activeUser,
862             'test_container_request',
863             'arvados/jobs',
864             ['echo', 'hello world'],
865             false, 'Committed')
866         .as('containerRequest');
867
868         cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
869             cy.goToPath(`/processes/${containerRequest.uuid}`);
870             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
871                 .parents('[data-cy=process-io-card]').within(() => {
872                     cy.wait(2000);
873                     cy.waitForDom();
874                     verifyIOParameter('input_file', null, "Label Description", 'input1.tar', 'keep:00000000000000000000000000000000+01');
875                     verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', 'keep:00000000000000000000000000000000+01', true);
876                     verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', 'keep:00000000000000000000000000000000+01', true);
877                     verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', 'keep:00000000000000000000000000000000+01', true);
878                     verifyIOParameter('input_dir', null, "Doc Description", 'No value', 'keep:11111111111111111111111111111111+01');
879                     verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
880                     verifyIOParameter('input_int', null, null, '1');
881                     verifyIOParameter('input_long', null, null, '1');
882                     verifyIOParameter('input_float', null, null, '1.5');
883                     verifyIOParameter('input_double', null, null, '1.3');
884                     verifyIOParameter('input_string', null, null, 'Hello World');
885                     verifyIOParameter('input_file_array', null, null, 'input2.tar', 'keep:00000000000000000000000000000000+02');
886                     verifyIOParameter('input_file_array', null, null, 'input3.tar', 'keep:00000000000000000000000000000000+03', true);
887                     verifyIOParameter('input_file_array', null, null, 'input3-2.txt', 'keep:00000000000000000000000000000000+03', true);
888                     verifyIOParameter('input_dir_array', null, null, 'No value', 'keep:11111111111111111111111111111111+02');
889                     verifyIOParameter('input_dir_array', null, null, 'No value', 'keep:11111111111111111111111111111111+03', true);
890                     verifyIOParameter('input_int_array', null, null, ["1", "3", "5"]);
891                     verifyIOParameter('input_long_array', null, null, ["10", "20"]);
892                     verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6"]);
893                     verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3"]);
894                     verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!"]);
895                 });
896             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
897                 .parents('[data-cy=process-io-card]').within((ctx) => {
898                     cy.get(ctx).scrollIntoView();
899                     cy.waitForDom().get('[data-cy="io-preview-image-toggle"]').click();
900                     const outPdh = testOutputCollection.portable_data_hash;
901
902                     verifyIOParameter('output_file', null, "Label Description", 'cat.png', `keep:${outPdh}`);
903                     verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
904                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `keep:${outPdh}`);
905                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', `keep:${outPdh}`, true);
906                     verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', `keep:${outPdh}`, true);
907                     verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `keep:${outPdh}`);
908                     verifyIOParameter('output_bool', null, null, 'true');
909                     verifyIOParameter('output_int', null, null, '1');
910                     verifyIOParameter('output_long', null, null, '1');
911                     verifyIOParameter('output_float', null, null, '100.5');
912                     verifyIOParameter('output_double', null, null, '100.3');
913                     verifyIOParameter('output_string', null, null, 'Hello output');
914                     verifyIOParameter('output_file_array', null, null, 'output2.tar', `keep:${outPdh}`);
915                     verifyIOParameter('output_file_array', null, null, 'output3.tar', `keep:${outPdh}`, true);
916                     verifyIOParameter('output_dir_array', null, null, 'outdir2', `keep:${outPdh}`);
917                     verifyIOParameter('output_dir_array', null, null, 'outdir3', `keep:${outPdh}`, true);
918                     verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
919                     verifyIOParameter('output_long_array', null, null, ["51", "52"]);
920                     verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
921                     verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
922                     verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
923                 });
924         });
925     });
926
927     it('displays IO parameters with no value', function() {
928
929         const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
930         const fakeOutputPDH = '11111111111111111111111111111111+99/';
931
932         cy.loginAs(activeUser);
933
934         // Add output uuid and inputs to container request
935         cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
936             req.reply((res) => {
937                 res.body.output_uuid = fakeOutputUUID;
938                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
939                     content: {}
940                 };
941                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
942                     content: {
943                         $graph: [{
944                             id: "#main",
945                             inputs: testInputs.map((input) => (input.definition)),
946                             outputs: testOutputs.map((output) => (output.definition))
947                         }]
948                     }
949                 };
950             });
951         });
952
953         // Stub fake output collection
954         cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
955             statusCode: 200,
956             body: {
957                 uuid: fakeOutputUUID,
958                 portable_data_hash: fakeOutputPDH,
959             }
960         });
961
962         // Stub fake output json
963         cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
964             statusCode: 200,
965             body: {}
966         });
967
968         cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
969             // Stub webdav response, points to output json
970             cy.intercept({method: 'PROPFIND', url: '*'}, {
971                 statusCode: 200,
972                 body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
973             });
974         });
975
976         createContainerRequest(
977             activeUser,
978             'test_container_request',
979             'arvados/jobs',
980             ['echo', 'hello world'],
981             false, 'Committed')
982         .as('containerRequest');
983
984         cy.getAll('@containerRequest').then(function([containerRequest]) {
985             cy.goToPath(`/processes/${containerRequest.uuid}`);
986             cy.get('[data-cy=process-io-card] h6').contains('Inputs')
987                 .parents('[data-cy=process-io-card]').within(() => {
988                     cy.wait(2000);
989                     cy.waitForDom();
990                     cy.get('tbody tr').each((item) => {
991                         cy.wrap(item).contains('No value');
992                     });
993                 });
994             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
995                 .parents('[data-cy=process-io-card]').within(() => {
996                     cy.get('tbody tr').each((item) => {
997                         cy.wrap(item).contains('No value');
998                     });
999                 });
1000         });
1001     });
1002
1003 });