18874: Add 'services/workbench2/' from commit 'f6f88d9ca9cdeeeebfadcfe999789bfb9f69e5c6'
[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                 cy.loginAs(activeUser);
435                 cy.goToPath(`/processes/${containerRequest.uuid}`);
436                 cy.get('[data-cy=process-details]').should('contain', crName);
437                 cy.get('[data-cy=process-logs]')
438                     .should('contain', 'No logs yet')
439                     .and('not.contain', 'hello world');
440
441                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
442                     "2023-07-18T20:14:48.128642814Z hello world"
443                 ]).then(() => {
444                     cy.get('[data-cy=process-logs]', {timeout: 7000})
445                         .should('not.contain', 'No logs yet')
446                         .and('contain', 'hello world');
447                 });
448
449                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
450                     "2023-07-18T20:14:49.128642814Z hello new line"
451                 ]).then(() => {
452                     cy.get('[data-cy=process-logs]', {timeout: 7000})
453                         .should('not.contain', 'No logs yet')
454                         .and('contain', 'hello new line');
455                 });
456             });
457         });
458
459         it('filters process logs by event type', function() {
460             const nodeInfoLogs = [
461                 'Host Information',
462                 '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',
463                 'CPU Information',
464                 'processor  : 0',
465                 'vendor_id  : GenuineIntel',
466                 'cpu family : 6',
467                 'model      : 79',
468                 'model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz'
469             ];
470             const crunchRunLogs = [
471                 '2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection',
472                 '2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started',
473                 '2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)',
474                 '2022-03-22T13:56:26.244862836Z Executing container \'zzzzz-dz642-1wokwvcct9s9du3\' using docker runtime',
475                 '2022-03-22T13:56:26.245037738Z Executing on host \'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p\'',
476             ];
477             const stdoutLogs = [
478                 '2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.',
479                 '2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.',
480                 '2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.',
481                 '2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.',
482                 '2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.',
483                 '2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.',
484                 '2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.',
485                 '2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.',
486                 '2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.',
487                 '2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.',
488                 '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.',
489                 '2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.',
490                 '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.',
491                 '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.',
492                 '2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.',
493                 '2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.',
494                 '2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.',
495                 '2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.',
496             ];
497
498             createContainerRequest(
499                 activeUser,
500                 'test_container_request',
501                 'arvados/jobs',
502                 ['echo', 'hello world'],
503                 false, 'Committed')
504             .then(function(containerRequest) {
505                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as('nodeInfoLogs');
506                 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as('crunchRunLogs');
507                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as('stdoutLogs');
508
509                 cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
510                     cy.loginAs(activeUser);
511                     cy.goToPath(`/processes/${containerRequest.uuid}`);
512                     // Should show main logs by default
513                     cy.get('[data-cy=process-logs-filter]', {timeout: 7000}).should('contain', 'Main logs');
514                     cy.get('[data-cy=process-logs]')
515                         .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
516                         .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
517                         .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
518                     // Select 'All logs'
519                     cy.get('[data-cy=process-logs-filter]').click();
520                     cy.get('body').contains('li', 'All logs').click();
521                     cy.get('[data-cy=process-logs]')
522                         .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
523                         .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
524                         .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
525                     // Select 'node-info' logs
526                     cy.get('[data-cy=process-logs-filter]').click();
527                     cy.get('body').contains('li', 'node-info').click();
528                     cy.get('[data-cy=process-logs]')
529                         .should('not.contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
530                         .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
531                         .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
532                     // Select 'stdout' logs
533                     cy.get('[data-cy=process-logs-filter]').click();
534                     cy.get('body').contains('li', 'stdout').click();
535                     cy.get('[data-cy=process-logs]')
536                         .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
537                         .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
538                         .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
539                 });
540             });
541         });
542
543         it('sorts combined logs', function() {
544             const crName = 'test_container_request';
545             createContainerRequest(
546                 activeUser,
547                 crName,
548                 'arvados/jobs',
549                 ['echo', 'hello world'],
550                 false, 'Committed')
551             .then(function(containerRequest) {
552                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
553                     "3: nodeinfo 1",
554                     "2: nodeinfo 2",
555                     "1: nodeinfo 3",
556                     "2: nodeinfo 4",
557                     "3: nodeinfo 5",
558                 ]).as('node-info');
559
560                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
561                     "2023-07-18T20:14:48.128642814Z first",
562                     "2023-07-18T20:14:49.128642814Z third"
563                 ]).as('stdout');
564
565                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
566                     "2023-07-18T20:14:48.528642814Z second"
567                 ]).as('stderr');
568
569                 cy.loginAs(activeUser);
570                 cy.goToPath(`/processes/${containerRequest.uuid}`);
571                 cy.get('[data-cy=process-details]').should('contain', crName);
572                 cy.get('[data-cy=process-logs]')
573                     .should('contain', 'No logs yet');
574
575                 cy.getAll('@node-info', '@stdout', '@stderr').then(() => {
576                     // Verify sorted main logs
577                     cy.get('[data-cy=process-logs] pre', {timeout: 7000})
578                         .eq(0).should('contain', '2023-07-18T20:14:48.128642814Z first');
579                     cy.get('[data-cy=process-logs] pre')
580                         .eq(1).should('contain', '2023-07-18T20:14:48.528642814Z second');
581                     cy.get('[data-cy=process-logs] pre')
582                         .eq(2).should('contain', '2023-07-18T20:14:49.128642814Z third');
583
584                     // Switch to All logs
585                     cy.get('[data-cy=process-logs-filter]').click();
586                     cy.get('body').contains('li', 'All logs').click();
587                     // Verify non-sorted lines were preserved
588                     cy.get('[data-cy=process-logs] pre')
589                         .eq(0).should('contain', '3: nodeinfo 1');
590                     cy.get('[data-cy=process-logs] pre')
591                         .eq(1).should('contain', '2: nodeinfo 2');
592                     cy.get('[data-cy=process-logs] pre')
593                         .eq(2).should('contain', '1: nodeinfo 3');
594                     cy.get('[data-cy=process-logs] pre')
595                         .eq(3).should('contain', '2: nodeinfo 4');
596                     cy.get('[data-cy=process-logs] pre')
597                         .eq(4).should('contain', '3: nodeinfo 5');
598                     // Verify sorted logs
599                     cy.get('[data-cy=process-logs] pre')
600                         .eq(5).should('contain', '2023-07-18T20:14:48.128642814Z first');
601                     cy.get('[data-cy=process-logs] pre')
602                         .eq(6).should('contain', '2023-07-18T20:14:48.528642814Z second');
603                     cy.get('[data-cy=process-logs] pre')
604                         .eq(7).should('contain', '2023-07-18T20:14:49.128642814Z third');
605                 });
606             });
607         });
608
609         it('correctly generates sniplines', function() {
610             const SNIPLINE = `================ âœ€ ================ âœ€ ========= Some log(s) were skipped ========= âœ€ ================ âœ€ ================`;
611             const crName = 'test_container_request';
612             createContainerRequest(
613                 activeUser,
614                 crName,
615                 'arvados/jobs',
616                 ['echo', 'hello world'],
617                 false, 'Committed')
618             .then(function(containerRequest) {
619
620                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
621                     'X'.repeat(63999) + '_' +
622                     'O'.repeat(100) +
623                     '_' + 'X'.repeat(63999)
624                 ]).as('stdout');
625
626                 cy.loginAs(activeUser);
627                 cy.goToPath(`/processes/${containerRequest.uuid}`);
628                 cy.get('[data-cy=process-details]').should('contain', crName);
629                 cy.get('[data-cy=process-logs]')
630                     .should('contain', 'No logs yet');
631
632                 // Switch to stdout since lines are unsortable (no timestamp)
633                 cy.get('[data-cy=process-logs-filter]').click();
634                 cy.get('body').contains('li', 'stdout').click();
635
636                 cy.getAll('@stdout').then(() => {
637                     // Verify first 64KB and snipline
638                     cy.get('[data-cy=process-logs] pre', {timeout: 7000})
639                         .eq(0).should('contain', 'X'.repeat(63999) + '_\n' + SNIPLINE);
640                     // Verify last 64KB
641                     cy.get('[data-cy=process-logs] pre')
642                         .eq(1).should('contain', '_' + 'X'.repeat(63999));
643                     // Verify none of the Os got through
644                     cy.get('[data-cy=process-logs] pre')
645                         .should('not.contain', 'O');
646                 });
647             });
648         });
649
650     });
651
652     describe('I/O panel', function() {
653         const testInputs = [
654             {
655                 definition: {
656                     "id": "#main/input_file",
657                     "label": "Label Description",
658                     "type": "File"
659                 },
660                 input: {
661                     "input_file": {
662                         "basename": "input1.tar",
663                         "class": "File",
664                         "location": "keep:00000000000000000000000000000000+01/input1.tar",
665                         "secondaryFiles": [
666                             {
667                                 "basename": "input1-2.txt",
668                                 "class": "File",
669                                 "location": "keep:00000000000000000000000000000000+01/input1-2.txt"
670                             },
671                             {
672                                 "basename": "input1-3.txt",
673                                 "class": "File",
674                                 "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
675                             },
676                             {
677                                 "basename": "input1-4.txt",
678                                 "class": "File",
679                                 "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
680                             }
681                         ]
682                     }
683                 }
684             },
685             {
686                 definition: {
687                     "id": "#main/input_dir",
688                     "doc": "Doc Description",
689                     "type": "Directory"
690                 },
691                 input: {
692                     "input_dir": {
693                         "basename": "11111111111111111111111111111111+01",
694                         "class": "Directory",
695                         "location": "keep:11111111111111111111111111111111+01"
696                     }
697                 }
698             },
699             {
700                 definition: {
701                     "id": "#main/input_bool",
702                     "doc": ["Doc desc 1", "Doc desc 2"],
703                     "type": "boolean"
704                 },
705                 input: {
706                     "input_bool": true,
707                 }
708             },
709             {
710                 definition: {
711                     "id": "#main/input_int",
712                     "type": "int"
713                 },
714                 input: {
715                     "input_int": 1,
716                 }
717             },
718             {
719                 definition: {
720                     "id": "#main/input_long",
721                     "type": "long"
722                 },
723                 input: {
724                     "input_long" : 1,
725                 }
726             },
727             {
728                 definition: {
729                     "id": "#main/input_float",
730                     "type": "float"
731                 },
732                 input: {
733                     "input_float": 1.5,
734                 }
735             },
736             {
737                 definition: {
738                     "id": "#main/input_double",
739                     "type": "double"
740                 },
741                 input: {
742                     "input_double": 1.3,
743                 }
744             },
745             {
746                 definition: {
747                     "id": "#main/input_string",
748                     "type": "string"
749                 },
750                 input: {
751                     "input_string": "Hello World",
752                 }
753             },
754             {
755                 definition: {
756                     "id": "#main/input_file_array",
757                     "type": {
758                       "items": "File",
759                       "type": "array"
760                     }
761                 },
762                 input: {
763                     "input_file_array": [
764                         {
765                             "basename": "input2.tar",
766                             "class": "File",
767                             "location": "keep:00000000000000000000000000000000+02/input2.tar"
768                         },
769                         {
770                             "basename": "input3.tar",
771                             "class": "File",
772                             "location": "keep:00000000000000000000000000000000+03/input3.tar",
773                             "secondaryFiles": [
774                                 {
775                                     "basename": "input3-2.txt",
776                                     "class": "File",
777                                     "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
778                                 }
779                             ]
780                         },
781                         {
782                             "$import": "import_path"
783                         }
784                     ]
785                 }
786             },
787             {
788                 definition: {
789                     "id": "#main/input_dir_array",
790                     "type": {
791                       "items": "Directory",
792                       "type": "array"
793                     }
794                 },
795                 input: {
796                     "input_dir_array": [
797                         {
798                             "basename": "11111111111111111111111111111111+02",
799                             "class": "Directory",
800                             "location": "keep:11111111111111111111111111111111+02"
801                         },
802                         {
803                             "basename": "11111111111111111111111111111111+03",
804                             "class": "Directory",
805                             "location": "keep:11111111111111111111111111111111+03"
806                         },
807                         {
808                             "$import": "import_path"
809                         }
810                     ]
811                 }
812             },
813             {
814                 definition: {
815                     "id": "#main/input_int_array",
816                     "type": {
817                       "items": "int",
818                       "type": "array"
819                     }
820                 },
821                 input: {
822                     "input_int_array": [
823                         1,
824                         3,
825                         5,
826                         {
827                             "$import": "import_path"
828                         }
829                     ]
830                 }
831             },
832             {
833                 definition: {
834                     "id": "#main/input_long_array",
835                     "type": {
836                       "items": "long",
837                       "type": "array"
838                     }
839                 },
840                 input: {
841                     "input_long_array": [
842                         10,
843                         20,
844                         {
845                             "$import": "import_path"
846                         }
847                     ]
848                 }
849             },
850             {
851                 definition: {
852                     "id": "#main/input_float_array",
853                     "type": {
854                       "items": "float",
855                       "type": "array"
856                     }
857                 },
858                 input: {
859                     "input_float_array": [
860                         10.2,
861                         10.4,
862                         10.6,
863                         {
864                             "$import": "import_path"
865                         }
866                     ]
867                 }
868             },
869             {
870                 definition: {
871                     "id": "#main/input_double_array",
872                     "type": {
873                       "items": "double",
874                       "type": "array"
875                     }
876                 },
877                 input: {
878                     "input_double_array": [
879                         20.1,
880                         20.2,
881                         20.3,
882                         {
883                             "$import": "import_path"
884                         }
885                     ]
886                 }
887             },
888             {
889                 definition: {
890                     "id": "#main/input_string_array",
891                     "type": {
892                       "items": "string",
893                       "type": "array"
894                     }
895                 },
896                 input: {
897                     "input_string_array": [
898                         "Hello",
899                         "World",
900                         "!",
901                         {
902                             "$import": "import_path"
903                         }
904                     ]
905                 }
906             },
907             {
908                 definition: {
909                     "id": "#main/input_bool_include",
910                     "type": "boolean"
911                 },
912                 input: {
913                     "input_bool_include": {
914                         "$include": "include_path"
915                     }
916                 }
917             },
918             {
919                 definition: {
920                     "id": "#main/input_int_include",
921                     "type": "int"
922                 },
923                 input: {
924                     "input_int_include": {
925                         "$include": "include_path"
926                     }
927                 }
928             },
929             {
930                 definition: {
931                     "id": "#main/input_float_include",
932                     "type": "float"
933                 },
934                 input: {
935                     "input_float_include": {
936                         "$include": "include_path"
937                     }
938                 }
939             },
940             {
941                 definition: {
942                     "id": "#main/input_string_include",
943                     "type": "string"
944                 },
945                 input: {
946                     "input_string_include": {
947                         "$include": "include_path"
948                     }
949                 }
950             },
951             {
952                 definition: {
953                     "id": "#main/input_file_include",
954                     "type": "File"
955                 },
956                 input: {
957                     "input_file_include": {
958                         "$include": "include_path"
959                     }
960                 }
961             },
962             {
963                 definition: {
964                     "id": "#main/input_directory_include",
965                     "type": "Directory"
966                 },
967                 input: {
968                     "input_directory_include": {
969                         "$include": "include_path"
970                     }
971                 }
972             },
973             {
974                 definition: {
975                     "id": "#main/input_file_url",
976                     "type": "File"
977                 },
978                 input: {
979                     "input_file_url": {
980                         "basename": "index.html",
981                         "class": "File",
982                         "location": "http://example.com/index.html"
983                       }
984                 }
985             }
986         ];
987
988         const testOutputs = [
989             {
990                 definition: {
991                     "id": "#main/output_file",
992                     "label": "Label Description",
993                     "type": "File"
994                 },
995                 output: {
996                     "output_file": {
997                         "basename": "cat.png",
998                         "class": "File",
999                         "location": "cat.png"
1000                     }
1001                 }
1002             },
1003             {
1004                 definition: {
1005                     "id": "#main/output_file_with_secondary",
1006                     "doc": "Doc Description",
1007                     "type": "File"
1008                 },
1009                 output: {
1010                     "output_file_with_secondary": {
1011                         "basename": "main.dat",
1012                         "class": "File",
1013                         "location": "main.dat",
1014                         "secondaryFiles": [
1015                             {
1016                                 "basename": "secondary.dat",
1017                                 "class": "File",
1018                                 "location": "secondary.dat"
1019                             },
1020                             {
1021                                 "basename": "secondary2.dat",
1022                                 "class": "File",
1023                                 "location": "secondary2.dat"
1024                             }
1025                         ]
1026                     }
1027                 }
1028             },
1029             {
1030                 definition: {
1031                     "id": "#main/output_dir",
1032                     "doc": ["Doc desc 1", "Doc desc 2"],
1033                     "type": "Directory"
1034                 },
1035                 output: {
1036                     "output_dir": {
1037                         "basename": "outdir1",
1038                         "class": "Directory",
1039                         "location": "outdir1"
1040                     }
1041                 }
1042             },
1043             {
1044                 definition: {
1045                     "id": "#main/output_bool",
1046                     "type": "boolean"
1047                 },
1048                 output: {
1049                     "output_bool": true
1050                 }
1051             },
1052             {
1053                 definition: {
1054                     "id": "#main/output_int",
1055                     "type": "int"
1056                 },
1057                 output: {
1058                     "output_int": 1
1059                 }
1060             },
1061             {
1062                 definition: {
1063                     "id": "#main/output_long",
1064                     "type": "long"
1065                 },
1066                 output: {
1067                     "output_long": 1
1068                 }
1069             },
1070             {
1071                 definition: {
1072                     "id": "#main/output_float",
1073                     "type": "float"
1074                 },
1075                 output: {
1076                     "output_float": 100.5
1077                 }
1078             },
1079             {
1080                 definition: {
1081                     "id": "#main/output_double",
1082                     "type": "double"
1083                 },
1084                 output: {
1085                     "output_double": 100.3
1086                 }
1087             },
1088             {
1089                 definition: {
1090                     "id": "#main/output_string",
1091                     "type": "string"
1092                 },
1093                 output: {
1094                     "output_string": "Hello output"
1095                 }
1096             },
1097             {
1098                 definition: {
1099                     "id": "#main/output_file_array",
1100                     "type": {
1101                         "items": "File",
1102                         "type": "array"
1103                     }
1104                 },
1105                 output: {
1106                     "output_file_array": [
1107                         {
1108                             "basename": "output2.tar",
1109                             "class": "File",
1110                             "location": "output2.tar"
1111                         },
1112                         {
1113                             "basename": "output3.tar",
1114                             "class": "File",
1115                             "location": "output3.tar"
1116                         }
1117                     ]
1118                 }
1119             },
1120             {
1121                 definition: {
1122                     "id": "#main/output_dir_array",
1123                     "type": {
1124                         "items": "Directory",
1125                         "type": "array"
1126                     }
1127                 },
1128                 output: {
1129                     "output_dir_array": [
1130                         {
1131                             "basename": "outdir2",
1132                             "class": "Directory",
1133                             "location": "outdir2"
1134                         },
1135                         {
1136                             "basename": "outdir3",
1137                             "class": "Directory",
1138                             "location": "outdir3"
1139                         }
1140                     ]
1141                 }
1142             },
1143             {
1144                 definition: {
1145                     "id": "#main/output_int_array",
1146                     "type": {
1147                         "items": "int",
1148                         "type": "array"
1149                     }
1150                 },
1151                 output: {
1152                     "output_int_array": [
1153                         10,
1154                         11,
1155                         12
1156                     ]
1157                 }
1158             },
1159             {
1160                 definition: {
1161                     "id": "#main/output_long_array",
1162                     "type": {
1163                         "items": "long",
1164                         "type": "array"
1165                     }
1166                 },
1167                 output: {
1168                     "output_long_array": [
1169                         51,
1170                         52
1171                     ]
1172                 }
1173             },
1174             {
1175                 definition: {
1176                     "id": "#main/output_float_array",
1177                     "type": {
1178                         "items": "float",
1179                         "type": "array"
1180                     }
1181                 },
1182                 output: {
1183                     "output_float_array": [
1184                         100.2,
1185                         100.4,
1186                         100.6
1187                     ]
1188                 }
1189             },
1190             {
1191                 definition: {
1192                     "id": "#main/output_double_array",
1193                     "type": {
1194                         "items": "double",
1195                         "type": "array"
1196                     }
1197                 },
1198                 output: {
1199                     "output_double_array": [
1200                         100.1,
1201                         100.2,
1202                         100.3
1203                     ]
1204                 }
1205             },
1206             {
1207                 definition: {
1208                     "id": "#main/output_string_array",
1209                     "type": {
1210                         "items": "string",
1211                         "type": "array"
1212                     }
1213                 },
1214                 output: {
1215                     "output_string_array": [
1216                         "Hello",
1217                         "Output",
1218                         "!"
1219                     ]
1220                 }
1221             }
1222         ];
1223
1224         const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1225             cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
1226                 label && cy.contains(label);
1227
1228                 if (multipleRows) {
1229                     cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
1230                     if (val) {
1231                         if (Array.isArray(val)) {
1232                             val.forEach(v => cy.get('@secondaryRows').contains(v));
1233                         } else {
1234                             cy.get('@secondaryRows').contains(val);
1235                         }
1236                     }
1237                     if (collection) {
1238                         cy.get('@secondaryRows').contains(collection);
1239                     }
1240                 } else {
1241                     if (val) {
1242                         if (Array.isArray(val)) {
1243                             val.forEach(v => cy.contains(v));
1244                         } else {
1245                             cy.contains(val);
1246                         }
1247                     }
1248                     if (collection) {
1249                         cy.contains(collection);
1250                     }
1251                 }
1252
1253
1254             });
1255         };
1256
1257         const verifyIOParameterImage = (name, url) => {
1258             cy.get('table tr').contains(name).parents('tr').within(() => {
1259                 cy.get('[alt="Inline Preview"]')
1260                     .should('be.visible')
1261                     .and(($img) => {
1262                         expect($img[0].naturalWidth).to.be.greaterThan(0);
1263                         expect($img[0].src).contains(url);
1264                     })
1265             });
1266         };
1267
1268         it('displays IO parameters with keep links and previews', function() {
1269             // Create output collection for real files
1270             cy.createCollection(adminUser.token, {
1271                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1272                 owner_uuid: activeUser.user.uuid,
1273             }).then((testOutputCollection) => {
1274                         cy.loginAs(activeUser);
1275
1276                         cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1277
1278                         cy.get('[data-cy=upload-button]').click();
1279
1280                         cy.fixture('files/cat.png', 'base64').then(content => {
1281                             cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
1282                             cy.get('[data-cy=form-submit-btn]').click();
1283                             cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
1284                             // Confirm final collection state.
1285                             cy.get('[data-cy=collection-files-panel]')
1286                                 .contains('cat.png').should('exist');
1287                         });
1288
1289                         cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
1290                     });
1291
1292             // Get updated collection pdh
1293             cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
1294                 // Add output uuid and inputs to container request
1295                 cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1296                     req.reply((res) => {
1297                         res.body.output_uuid = testOutputCollection.uuid;
1298                         res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1299                             content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
1300                         };
1301                         res.body.mounts["/var/lib/cwl/workflow.json"] = {
1302                             content: {
1303                                 $graph: [{
1304                                     id: "#main",
1305                                     inputs: testInputs.map((input) => (input.definition)),
1306                                     outputs: testOutputs.map((output) => (output.definition))
1307                                 }]
1308                             }
1309                         };
1310                     });
1311                 });
1312
1313                 // Stub fake output collection
1314                 cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
1315                     statusCode: 200,
1316                     body: {
1317                         uuid: testOutputCollection.uuid,
1318                         portable_data_hash: testOutputCollection.portable_data_hash,
1319                     }
1320                 });
1321
1322                 // Stub fake output json
1323                 cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
1324                     statusCode: 200,
1325                     body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
1326                 });
1327
1328                 // Stub webdav response, points to output json
1329                 cy.intercept({method: 'PROPFIND', url: '*'}, {
1330                     fixture: 'webdav-propfind-outputs.xml',
1331                 });
1332             });
1333
1334             createContainerRequest(
1335                 activeUser,
1336                 'test_container_request',
1337                 'arvados/jobs',
1338                 ['echo', 'hello world'],
1339                 false, 'Committed')
1340             .as('containerRequest');
1341
1342             cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
1343                 cy.goToPath(`/processes/${containerRequest.uuid}`);
1344                 cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1345                     .parents('[data-cy=process-io-card]').within(() => {
1346                         verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
1347                         verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
1348                         verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
1349                         verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
1350                         verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
1351                         verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
1352                         verifyIOParameter('input_int', null, null, '1');
1353                         verifyIOParameter('input_long', null, null, '1');
1354                         verifyIOParameter('input_float', null, null, '1.5');
1355                         verifyIOParameter('input_double', null, null, '1.3');
1356                         verifyIOParameter('input_string', null, null, 'Hello World');
1357                         verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
1358                         verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
1359                         verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
1360                         verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
1361                         verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
1362                         verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
1363                         verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
1364                         verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
1365                         verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
1366                         verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1367                         verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1368                         verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
1369                         verifyIOParameter('input_bool_include', null, null, "Cannot display value");
1370                         verifyIOParameter('input_int_include', null, null, "Cannot display value");
1371                         verifyIOParameter('input_float_include', null, null, "Cannot display value");
1372                         verifyIOParameter('input_string_include', null, null, "Cannot display value");
1373                         verifyIOParameter('input_file_include', null, null, "Cannot display value");
1374                         verifyIOParameter('input_directory_include', null, null, "Cannot display value");
1375                         verifyIOParameter('input_file_url', null, null, "http://example.com/index.html");
1376                     });
1377                 cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1378                     .parents('[data-cy=process-io-card]').within((ctx) => {
1379                         cy.get(ctx).scrollIntoView();
1380                         cy.get('[data-cy="io-preview-image-toggle"]').click({waitForAnimations: false});
1381                         const outPdh = testOutputCollection.portable_data_hash;
1382
1383                         verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
1384                         verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
1385                         verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
1386                         verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
1387                         verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
1388                         verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
1389                         verifyIOParameter('output_bool', null, null, 'true');
1390                         verifyIOParameter('output_int', null, null, '1');
1391                         verifyIOParameter('output_long', null, null, '1');
1392                         verifyIOParameter('output_float', null, null, '100.5');
1393                         verifyIOParameter('output_double', null, null, '100.3');
1394                         verifyIOParameter('output_string', null, null, 'Hello output');
1395                         verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
1396                         verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
1397                         verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
1398                         verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
1399                         verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
1400                         verifyIOParameter('output_long_array', null, null, ["51", "52"]);
1401                         verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
1402                         verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
1403                         verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
1404                     });
1405             });
1406         });
1407
1408         it('displays IO parameters with no value', function() {
1409
1410             const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
1411             const fakeOutputPDH = '11111111111111111111111111111111+99/';
1412
1413             cy.loginAs(activeUser);
1414
1415             // Add output uuid and inputs to container request
1416             cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
1417                 req.reply((res) => {
1418                     res.body.output_uuid = fakeOutputUUID;
1419                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1420                         content: {}
1421                     };
1422                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
1423                         content: {
1424                             $graph: [{
1425                                 id: "#main",
1426                                 inputs: testInputs.map((input) => (input.definition)),
1427                                 outputs: testOutputs.map((output) => (output.definition))
1428                             }]
1429                         }
1430                     };
1431                 });
1432             });
1433
1434             // Stub fake output collection
1435             cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
1436                 statusCode: 200,
1437                 body: {
1438                     uuid: fakeOutputUUID,
1439                     portable_data_hash: fakeOutputPDH,
1440                 }
1441             });
1442
1443             // Stub fake output json
1444             cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
1445                 statusCode: 200,
1446                 body: {}
1447             });
1448
1449             cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
1450                 // Stub webdav response, points to output json
1451                 cy.intercept({method: 'PROPFIND', url: '*'}, {
1452                     statusCode: 200,
1453                     body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
1454                 });
1455             });
1456
1457             createContainerRequest(
1458                 activeUser,
1459                 'test_container_request',
1460                 'arvados/jobs',
1461                 ['echo', 'hello world'],
1462                 false, 'Committed')
1463             .as('containerRequest');
1464
1465             cy.getAll('@containerRequest').then(function([containerRequest]) {
1466                 cy.goToPath(`/processes/${containerRequest.uuid}`);
1467                 cy.get('[data-cy=process-io-card] h6').contains('Inputs')
1468                     .parents('[data-cy=process-io-card]').within(() => {
1469                         cy.wait(2000);
1470                         cy.waitForDom();
1471                         cy.get('tbody tr').each((item) => {
1472                             cy.wrap(item).contains('No value');
1473                         });
1474                     });
1475                 cy.get('[data-cy=process-io-card] h6').contains('Outputs')
1476                     .parents('[data-cy=process-io-card]').within(() => {
1477                         cy.get('tbody tr').each((item) => {
1478                             cy.wrap(item).contains('No value');
1479                         });
1480                     });
1481             });
1482         });
1483     });
1484
1485 });