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