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