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