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