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