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