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