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