From 49bd8985e274a0864ebf7b82a4b756abe3472e0a Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 14:40:44 +0200 Subject: [PATCH 1/7] Reorganize test data --- .../clean-diff/workflow_with_revisions.ga | 0 test-data/json/clean/wf_01_clean.ga | 106 ++++++++++++++++++ test-data/json/clean/wf_01_dirty.ga | 106 ++++++++++++++++++ test-data/json/validation/test_wf_01.ga | 87 ++++++++++++++ .../{ => yaml}/test_workflow_01.gxwf.yml | 0 5 files changed, 299 insertions(+) rename test-data/{ => json}/clean-diff/workflow_with_revisions.ga (100%) create mode 100644 test-data/json/clean/wf_01_clean.ga create mode 100644 test-data/json/clean/wf_01_dirty.ga create mode 100644 test-data/json/validation/test_wf_01.ga rename test-data/{ => yaml}/test_workflow_01.gxwf.yml (100%) diff --git a/test-data/clean-diff/workflow_with_revisions.ga b/test-data/json/clean-diff/workflow_with_revisions.ga similarity index 100% rename from test-data/clean-diff/workflow_with_revisions.ga rename to test-data/json/clean-diff/workflow_with_revisions.ga diff --git a/test-data/json/clean/wf_01_clean.ga b/test-data/json/clean/wf_01_clean.ga new file mode 100644 index 0000000..ffecb6a --- /dev/null +++ b/test-data/json/clean/wf_01_clean.ga @@ -0,0 +1,106 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "This is a cool workflow", + "creator": [ + { + "class": "Person", + "identifier": "My ID", + "name": "Tester" + } + ], + "format-version": "0.1", + "license": "MIT", + "name": "Cool workflow", + "steps": { + "0": { + "annotation": "Some text", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "Some text", + "name": "The cool input" + } + ], + "label": "The cool input", + "name": "Input dataset", + "outputs": [], + "position": { + "bottom": 610.28125, + "height": 61.78125, + "left": 824, + "right": 1024, + "top": 548.5, + "width": 200, + "x": 824, + "y": 548.5 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": \"\"}", + "tool_version": null, + "type": "data_input", + "uuid": "f0019f6a-b5ee-476e-8e5f-11cd3bcfaccf", + "workflow_outputs": [ + { + "label": null, + "output_name": "output", + "uuid": "2216c468-98e3-42f9-9174-1e4b65f07e8e" + } + ] + }, + "1": { + "annotation": "", + "content_id": "wc_gnu", + "errors": null, + "id": 1, + "input_connections": { + "input1": { + "id": 0, + "output_name": "output" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool Line/Word/Character count", + "name": "input1" + } + ], + "label": null, + "name": "Line/Word/Character count", + "outputs": [ + { + "name": "out_file1", + "type": "tabular" + } + ], + "position": { + "bottom": 781.453125, + "height": 133.953125, + "left": 1146, + "right": 1346, + "top": 647.5, + "width": 200, + "x": 1146, + "y": 647.5 + }, + "post_job_actions": {}, + "tool_id": "wc_gnu", + "tool_state": "{\"include_header\": \"true\", \"input1\": {\"__class__\": \"RuntimeValue\"}, \"options\": [\"lines\", \"words\", \"characters\"], \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.0.0", + "type": "tool", + "uuid": "1ca6f40f-7b65-46ae-a366-9592279a07cc", + "workflow_outputs": [ + { + "label": "Text Count Result", + "output_name": "out_file1", + "uuid": "d9a1c36f-bb05-4dc8-b6d8-10b94ddffb77" + } + ] + } + }, + "tags": [], + "uuid": "1bcc69fb-e8ec-49db-bd60-c7ac7fa87838", + "version": 2 +} diff --git a/test-data/json/clean/wf_01_dirty.ga b/test-data/json/clean/wf_01_dirty.ga new file mode 100644 index 0000000..ffecb6a --- /dev/null +++ b/test-data/json/clean/wf_01_dirty.ga @@ -0,0 +1,106 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "This is a cool workflow", + "creator": [ + { + "class": "Person", + "identifier": "My ID", + "name": "Tester" + } + ], + "format-version": "0.1", + "license": "MIT", + "name": "Cool workflow", + "steps": { + "0": { + "annotation": "Some text", + "content_id": null, + "errors": null, + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "Some text", + "name": "The cool input" + } + ], + "label": "The cool input", + "name": "Input dataset", + "outputs": [], + "position": { + "bottom": 610.28125, + "height": 61.78125, + "left": 824, + "right": 1024, + "top": 548.5, + "width": 200, + "x": 824, + "y": 548.5 + }, + "tool_id": null, + "tool_state": "{\"optional\": false, \"tag\": \"\"}", + "tool_version": null, + "type": "data_input", + "uuid": "f0019f6a-b5ee-476e-8e5f-11cd3bcfaccf", + "workflow_outputs": [ + { + "label": null, + "output_name": "output", + "uuid": "2216c468-98e3-42f9-9174-1e4b65f07e8e" + } + ] + }, + "1": { + "annotation": "", + "content_id": "wc_gnu", + "errors": null, + "id": 1, + "input_connections": { + "input1": { + "id": 0, + "output_name": "output" + } + }, + "inputs": [ + { + "description": "runtime parameter for tool Line/Word/Character count", + "name": "input1" + } + ], + "label": null, + "name": "Line/Word/Character count", + "outputs": [ + { + "name": "out_file1", + "type": "tabular" + } + ], + "position": { + "bottom": 781.453125, + "height": 133.953125, + "left": 1146, + "right": 1346, + "top": 647.5, + "width": 200, + "x": 1146, + "y": 647.5 + }, + "post_job_actions": {}, + "tool_id": "wc_gnu", + "tool_state": "{\"include_header\": \"true\", \"input1\": {\"__class__\": \"RuntimeValue\"}, \"options\": [\"lines\", \"words\", \"characters\"], \"__page__\": null, \"__rerun_remap_job_id__\": null}", + "tool_version": "1.0.0", + "type": "tool", + "uuid": "1ca6f40f-7b65-46ae-a366-9592279a07cc", + "workflow_outputs": [ + { + "label": "Text Count Result", + "output_name": "out_file1", + "uuid": "d9a1c36f-bb05-4dc8-b6d8-10b94ddffb77" + } + ] + } + }, + "tags": [], + "uuid": "1bcc69fb-e8ec-49db-bd60-c7ac7fa87838", + "version": 2 +} diff --git a/test-data/json/validation/test_wf_01.ga b/test-data/json/validation/test_wf_01.ga new file mode 100644 index 0000000..b45ce03 --- /dev/null +++ b/test-data/json/validation/test_wf_01.ga @@ -0,0 +1,87 @@ +{ + "a_galaxy_workflow": "true", + "annotation": "simple workflow", + "format-version": "0.1", + "name": "TestWorkflow1", + "steps": { + "0": { + "annotation": "input1 description", + "id": 0, + "input_connections": {}, + "inputs": [ + { + "description": "input1 description", + "name": "WorkflowInput1" + } + ], + "name": "Input dataset", + "outputs": [], + "position": { + "left": 199.55555772781372, + "top": 200.66666460037231 + }, + "tool_errors": null, + "tool_id": null, + "tool_state": "{\"name\": \"WorkflowInput1\"}", + "tool_version": null, + "type": "data_input", + "user_outputs": [] + }, + "1": { + "annotation": "", + "id": 1, + "input_connections": {}, + "inputs": [ + { + "description": "", + "name": "WorkflowInput2" + } + ], + "name": "Input dataset", + "outputs": [], + "position": { + "left": 206.22221422195435, + "top": 327.33335161209106 + }, + "tool_errors": null, + "tool_id": null, + "tool_state": "{\"name\": \"WorkflowInput2\"}", + "tool_version": null, + "type": "data_input", + "user_outputs": [] + }, + "2": { + "annotation": "", + "id": 2, + "input_connections": { + "input1": { + "id": 0, + "output_name": "output" + }, + "queries_0|input2": { + "id": 1, + "output_name": "output" + } + }, + "inputs": [], + "name": "Concatenate datasets", + "outputs": [ + { + "name": "out_file1", + "type": "input" + } + ], + "position": { + "left": 419.33335876464844, + "top": 200.44446563720703 + }, + "post_job_actions": {}, + "tool_errors": null, + "tool_id": "cat1", + "tool_state": "{\"__page__\": 0, \"__rerun_remap_job_id__\": null, \"input1\": \"null\", \"queries\": \"[{\\\"input2\\\": null, \\\"__index__\\\": 0}]\"}", + "tool_version": "1.0.0", + "type": "tool", + "user_outputs": [] + } + } +} diff --git a/test-data/test_workflow_01.gxwf.yml b/test-data/yaml/test_workflow_01.gxwf.yml similarity index 100% rename from test-data/test_workflow_01.gxwf.yml rename to test-data/yaml/test_workflow_01.gxwf.yml From e2d4506214ea20d0e67087135a61773503d46d0d Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 16:09:35 +0200 Subject: [PATCH 2/7] Add some integration tests helper functions --- client/tests/e2e/suite/helpers.ts | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 client/tests/e2e/suite/helpers.ts diff --git a/client/tests/e2e/suite/helpers.ts b/client/tests/e2e/suite/helpers.ts new file mode 100644 index 0000000..a7a28ff --- /dev/null +++ b/client/tests/e2e/suite/helpers.ts @@ -0,0 +1,78 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import * as assert from "assert"; + +const GALAXY_WORKFLOWS_EXTENSION_ID = "davelopez.galaxy-workflows"; + +/** + * Contains the document and its corresponding editor + */ +export interface DocumentEditor { + editor: vscode.TextEditor; + document: vscode.TextDocument; +} + +export async function activate(): Promise { + const ext = vscode.extensions.getExtension(GALAXY_WORKFLOWS_EXTENSION_ID); + const api = ext.isActive ? ext.exports : await ext.activate(); + assert.ok(api); + return api; +} + +export async function openDocumentInEditor(docUri: vscode.Uri): Promise { + try { + const document = await vscode.workspace.openTextDocument(docUri); + const editor = await vscode.window.showTextDocument(document); + return { + editor, + document, + }; + } catch (e) { + console.error(e); + } +} + +export async function activateAndOpen(docUri: vscode.Uri): Promise { + await activate(); + const documentEditor = await openDocumentInEditor(docUri); + await sleep(2000); // Wait for server activation + return documentEditor; +} + +export async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export const getDocPath = (filePath: string): string => { + return path.resolve(__dirname, path.join("..", "..", "..", "..", "test-data", filePath)); +}; + +export const getDocUri = (filePath: string): vscode.Uri => { + return vscode.Uri.file(getDocPath(filePath)); +}; + +export async function assertDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]): Promise { + const actualDiagnostics = vscode.languages.getDiagnostics(docUri); + + assert.equal(actualDiagnostics.length, expectedDiagnostics.length); + + expectedDiagnostics.forEach((expectedDiagnostic, i) => { + const actualDiagnostic = actualDiagnostics[i]; + assert.equal(actualDiagnostic.message, expectedDiagnostic.message); + assert.deepEqual(actualDiagnostic.range, expectedDiagnostic.range); + assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity); + }); +} + +/** + * Asserts that the given workflow document has no diagnostics i.e. is valid. + * @param docUri Workflow document URI + */ +export async function assertValid(docUri: vscode.Uri): Promise { + const actualDiagnostics = vscode.languages.getDiagnostics(docUri); + assert.equal(actualDiagnostics.length, 0); +} + +export function closeAllEditors(): Thenable { + return vscode.commands.executeCommand("workbench.action.closeAllEditors"); +} From 19ce5077f6679b67637178609091bd656fb3dad4 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 16:10:25 +0200 Subject: [PATCH 3/7] Increase timeout for integrations tests Helps during debugging --- client/tests/e2e/suite/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/tests/e2e/suite/index.ts b/client/tests/e2e/suite/index.ts index e0dc946..94b59e9 100644 --- a/client/tests/e2e/suite/index.ts +++ b/client/tests/e2e/suite/index.ts @@ -7,6 +7,7 @@ export function run(): Promise { const mocha = new Mocha({ ui: "tdd", color: true, + timeout: 60000, }); const testsRoot = path.resolve(__dirname, ".."); From 8290e0f75974013265abe0a45acbc61748168e63 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 16:12:32 +0200 Subject: [PATCH 4/7] Fix integration tests runner The `extensionDevelopmentPath` was not pointing to the right directory where the extension manifest file was in. --- client/tests/e2e/runTests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/tests/e2e/runTests.ts b/client/tests/e2e/runTests.ts index e810ed5..c871f34 100644 --- a/client/tests/e2e/runTests.ts +++ b/client/tests/e2e/runTests.ts @@ -2,11 +2,11 @@ import * as path from "path"; import { runTests } from "@vscode/test-electron"; -async function main() { +async function main(): Promise { try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.resolve(__dirname, "../../"); + const extensionDevelopmentPath = path.resolve(__dirname, "../../../"); // The path to test runner // Passed to --extensionTestsPath From 16b8fb9cae1a5540e0f96073650836215730e7d3 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 16:31:12 +0200 Subject: [PATCH 5/7] Add integration test for clean workflow command --- client/tests/e2e/suite/extension.e2e.ts | 27 +++++++++++----- client/tests/e2e/suite/helpers.ts | 16 +++++++--- client/tests/e2e/suite/index.ts | 1 + test-data/json/clean/wf_01_clean.ga | 41 ++----------------------- test-data/json/clean/wf_01_dirty.ga | 7 ----- 5 files changed, 35 insertions(+), 57 deletions(-) diff --git a/client/tests/e2e/suite/extension.e2e.ts b/client/tests/e2e/suite/extension.e2e.ts index d958ab4..817636c 100644 --- a/client/tests/e2e/suite/extension.e2e.ts +++ b/client/tests/e2e/suite/extension.e2e.ts @@ -1,14 +1,27 @@ -import * as assert from "assert"; - // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from "vscode"; -// import * as myExtension from '../../extension'; +import * as path from "path"; +import * as assert from "assert"; +import { activateAndOpenInEditor, getDocUri, closeAllEditors, openDocument, sleep } from "./helpers"; suite("Extension Test Suite", () => { - vscode.window.showInformationMessage("Start all tests."); - - test("Sample test", () => { - assert.strictEqual([1, 2, 3].indexOf(5), -1); + teardown(closeAllEditors); + suite("Native (JSON) Workflows", () => { + suite("Commands Tests", () => { + test("Clean workflow command removes non-essential properties", async () => { + const dirtyDocUri = getDocUri(path.join("json", "clean", "wf_01_dirty.ga")); + const cleanDocUri = getDocUri(path.join("json", "clean", "wf_01_clean.ga")); + const { document } = await activateAndOpenInEditor(dirtyDocUri); + const dirtyDoc = document.getText(); + await vscode.commands.executeCommand("galaxy-workflows.cleanWorkflow"); + await sleep(1000); // Wait for command to apply changes + const actualCleanJson = document.getText(); + assert.notEqual(dirtyDoc, actualCleanJson); + const expectedCleanDocument = await openDocument(cleanDocUri); + const expectedCleanJson = expectedCleanDocument.getText(); + assert.strictEqual(actualCleanJson, expectedCleanJson); + }); + }); }); }); diff --git a/client/tests/e2e/suite/helpers.ts b/client/tests/e2e/suite/helpers.ts index a7a28ff..a4b459d 100644 --- a/client/tests/e2e/suite/helpers.ts +++ b/client/tests/e2e/suite/helpers.ts @@ -2,8 +2,6 @@ import * as vscode from "vscode"; import * as path from "path"; import * as assert from "assert"; -const GALAXY_WORKFLOWS_EXTENSION_ID = "davelopez.galaxy-workflows"; - /** * Contains the document and its corresponding editor */ @@ -13,9 +11,8 @@ export interface DocumentEditor { } export async function activate(): Promise { - const ext = vscode.extensions.getExtension(GALAXY_WORKFLOWS_EXTENSION_ID); + const ext = vscode.extensions.getExtension("davelopez.galaxy-workflows"); const api = ext.isActive ? ext.exports : await ext.activate(); - assert.ok(api); return api; } @@ -32,7 +29,16 @@ export async function openDocumentInEditor(docUri: vscode.Uri): Promise { +export async function openDocument(docUri: vscode.Uri): Promise { + try { + const document = await vscode.workspace.openTextDocument(docUri); + return document; + } catch (e) { + console.error(e); + } +} + +export async function activateAndOpenInEditor(docUri: vscode.Uri): Promise { await activate(); const documentEditor = await openDocumentInEditor(docUri); await sleep(2000); // Wait for server activation diff --git a/client/tests/e2e/suite/index.ts b/client/tests/e2e/suite/index.ts index 94b59e9..e6f82f1 100644 --- a/client/tests/e2e/suite/index.ts +++ b/client/tests/e2e/suite/index.ts @@ -8,6 +8,7 @@ export function run(): Promise { ui: "tdd", color: true, timeout: 60000, + inlineDiffs: true, }); const testsRoot = path.resolve(__dirname, ".."); diff --git a/test-data/json/clean/wf_01_clean.ga b/test-data/json/clean/wf_01_clean.ga index ffecb6a..de9b3a0 100644 --- a/test-data/json/clean/wf_01_clean.ga +++ b/test-data/json/clean/wf_01_clean.ga @@ -1,13 +1,6 @@ { "a_galaxy_workflow": "true", "annotation": "This is a cool workflow", - "creator": [ - { - "class": "Person", - "identifier": "My ID", - "name": "Tester" - } - ], "format-version": "0.1", "license": "MIT", "name": "Cool workflow", @@ -15,7 +8,6 @@ "0": { "annotation": "Some text", "content_id": null, - "errors": null, "id": 0, "input_connections": {}, "inputs": [ @@ -27,33 +19,20 @@ "label": "The cool input", "name": "Input dataset", "outputs": [], - "position": { - "bottom": 610.28125, - "height": 61.78125, - "left": 824, - "right": 1024, - "top": 548.5, - "width": 200, - "x": 824, - "y": 548.5 - }, "tool_id": null, "tool_state": "{\"optional\": false, \"tag\": \"\"}", "tool_version": null, "type": "data_input", - "uuid": "f0019f6a-b5ee-476e-8e5f-11cd3bcfaccf", "workflow_outputs": [ { "label": null, - "output_name": "output", - "uuid": "2216c468-98e3-42f9-9174-1e4b65f07e8e" + "output_name": "output" } ] }, "1": { "annotation": "", "content_id": "wc_gnu", - "errors": null, "id": 1, "input_connections": { "input1": { @@ -75,32 +54,18 @@ "type": "tabular" } ], - "position": { - "bottom": 781.453125, - "height": 133.953125, - "left": 1146, - "right": 1346, - "top": 647.5, - "width": 200, - "x": 1146, - "y": 647.5 - }, "post_job_actions": {}, "tool_id": "wc_gnu", "tool_state": "{\"include_header\": \"true\", \"input1\": {\"__class__\": \"RuntimeValue\"}, \"options\": [\"lines\", \"words\", \"characters\"], \"__page__\": null, \"__rerun_remap_job_id__\": null}", "tool_version": "1.0.0", "type": "tool", - "uuid": "1ca6f40f-7b65-46ae-a366-9592279a07cc", "workflow_outputs": [ { "label": "Text Count Result", - "output_name": "out_file1", - "uuid": "d9a1c36f-bb05-4dc8-b6d8-10b94ddffb77" + "output_name": "out_file1" } ] } }, - "tags": [], - "uuid": "1bcc69fb-e8ec-49db-bd60-c7ac7fa87838", - "version": 2 + "tags": [] } diff --git a/test-data/json/clean/wf_01_dirty.ga b/test-data/json/clean/wf_01_dirty.ga index ffecb6a..ef79de5 100644 --- a/test-data/json/clean/wf_01_dirty.ga +++ b/test-data/json/clean/wf_01_dirty.ga @@ -1,13 +1,6 @@ { "a_galaxy_workflow": "true", "annotation": "This is a cool workflow", - "creator": [ - { - "class": "Person", - "identifier": "My ID", - "name": "Tester" - } - ], "format-version": "0.1", "license": "MIT", "name": "Cool workflow", From b8b119031705ddfdf1cc90b9ecd5a484ea0590b3 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 17:24:56 +0200 Subject: [PATCH 6/7] Ensure full build before running integration tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da7a1b5..53ab707 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "test-unit-client": "cd client && npm run test-unit && cd ..", "test-unit-server": "cd server && npm run test-unit && cd ..", "test-compile": "tsc --project ./client --outDir client/out", - "pretest:e2e": "npm run test-compile", + "pretest:e2e": "npm run compile && npm run test-compile", "test:e2e": "node ./client/out/e2e/runTests.js" }, "devDependencies": { From 2b33a20d8579210724943a728a69ed0e8e278e77 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 14 May 2022 17:27:41 +0200 Subject: [PATCH 7/7] Fix Clean workflow command Was failing because conflicting edits trying to remove the trailing comma for a property that was already removed. Now, if the property with the trailing comma is removed, it will check the previous property and so on. --- server/src/commands/cleanWorkflow.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/commands/cleanWorkflow.ts b/server/src/commands/cleanWorkflow.ts index 1432591..15307ea 100644 --- a/server/src/commands/cleanWorkflow.ts +++ b/server/src/commands/cleanWorkflow.ts @@ -105,7 +105,10 @@ export class CleanWorkflowCommand extends CustomCommand { // Remove trailing comma in previous property node const isLastNode = workflowDocument.isLastNodeInParent(node); if (isLastNode) { - const previousNode = workflowDocument.getPreviousSiblingNode(node); + let previousNode = workflowDocument.getPreviousSiblingNode(node); + while (previousNode && nodesToRemove.includes(previousNode)) { + previousNode = workflowDocument.getPreviousSiblingNode(previousNode); + } if (previousNode) { const range = this.getFullNodeRange(workflowDocument.textDocument, previousNode); const nodeText = workflowDocument.textDocument.getText(range); @@ -132,10 +135,7 @@ export class CleanWorkflowCommand extends CustomCommand { return result; } - private getNonEssentialNodes( - workflowDocument: WorkflowDocument, - cleanablePropertyNames: Set - ): PropertyASTNode[] { + private getNonEssentialNodes(workflowDocument: WorkflowDocument, cleanablePropertyNames: Set): ASTNode[] { const root = workflowDocument.rootNode; if (!root) { return [];