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