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