From 02568443fdd4d65a62b901a3d136e88673d61356 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Mon, 25 Jan 2021 19:18:28 +0200 Subject: [PATCH] add integration tests Signed-off-by: Jari Kolehmainen --- integration/__tests__/app.tests.ts | 541 +----------------- integration/__tests__/cluster-pages.tests.ts | 431 ++++++++++++++ .../__tests__/command-palette.tests.ts | 36 ++ integration/__tests__/workspace.tests.ts | 58 ++ integration/helpers/minikube.ts | 59 ++ integration/helpers/utils.ts | 22 + src/common/ipc.ts | 25 +- src/common/workspace-store.ts | 1 + .../components/+workspaces/add-workspace.tsx | 1 + .../+workspaces/remove-workspace.tsx | 1 + .../components/cluster-manager/bottom-bar.tsx | 4 +- .../command-palette/command-dialog.tsx | 1 + 12 files changed, 629 insertions(+), 551 deletions(-) create mode 100644 integration/__tests__/cluster-pages.tests.ts create mode 100644 integration/__tests__/command-palette.tests.ts create mode 100644 integration/__tests__/workspace.tests.ts create mode 100644 integration/helpers/minikube.ts diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index c986e4804e..21c832e803 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -1,12 +1,6 @@ -/* - Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE - namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the - TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube - cluster and vice versa. -*/ import { Application } from "spectron"; import * as utils from "../helpers/utils"; -import { spawnSync, exec } from "child_process"; +import { exec } from "child_process"; import * as util from "util"; export const promiseExec = util.promisify(exec); @@ -15,62 +9,10 @@ jest.setTimeout(60000); // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) describe("Lens integration tests", () => { - const TEST_NAMESPACE = "integration-tests"; - const BACKSPACE = "\uE003"; let app: Application; - const appStart = async () => { - app = utils.setup(); - await app.start(); - // Wait for splash screen to be closed - while (await app.client.getWindowCount() > 1); - await app.client.windowByIndex(0); - await app.client.waitUntilWindowLoaded(); - }; - const clickWhatsNew = async (app: Application) => { - await app.client.waitUntilTextExists("h1", "What's new?"); - await app.client.click("button.primary"); - await app.client.waitUntilTextExists("h1", "Welcome"); - }; - const minikubeReady = (): boolean => { - // determine if minikube is running - { - const { status } = spawnSync("minikube status", { shell: true }); - - if (status !== 0) { - console.warn("minikube not running"); - - return false; - } - } - - // Remove TEST_NAMESPACE if it already exists - { - const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); - - if (status === 0) { - console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); - - const { status, stdout, stderr } = spawnSync( - `minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, - { shell: true }, - ); - - if (status !== 0) { - console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`); - - return false; - } - - console.log(stdout.toString()); - } - } - - return true; - }; - const ready = minikubeReady(); describe("app start", () => { - beforeAll(appStart, 20000); + beforeAll(async () => app = await utils.appStart(), 20000); afterAll(async () => { if (app?.isRunning()) { @@ -79,7 +21,7 @@ describe("Lens integration tests", () => { }); it('shows "whats new"', async () => { - await clickWhatsNew(app); + await utils.clickWhatsNew(app); }); it('shows "add cluster"', async () => { @@ -96,10 +38,7 @@ describe("Lens integration tests", () => { }); it("ensures helm repos", async () => { - const { stdout: reposJson } = await promiseExec("helm repo list -o json"); - const repos = JSON.parse(reposJson); - - await app.client.waitUntilTextExists("div.repos #message-bitnami", repos[0].name); // wait for the helm-cli to fetch the repo(s) + await app.client.waitUntilTextExists("div.repos #message-bitnami", "bitnami"); // wait for the helm-cli to fetch the repo(s) await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) }); @@ -110,476 +49,4 @@ describe("Lens integration tests", () => { await app.client.keys("Meta"); }); }); - - utils.describeIf(ready)("workspaces", () => { - beforeAll(appStart, 20000); - - afterAll(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it("creates new workspace", async () => { - await clickWhatsNew(app); - await app.client.click("#current-workspace .Icon"); - await app.client.click('a[href="/workspaces"]'); - await app.client.click(".Workspaces button.Button"); - await app.client.keys("test-workspace"); - await app.client.click(".Workspaces .Input.description input"); - await app.client.keys("test description"); - await app.client.click(".Workspaces .workspace.editing .Icon"); - await app.client.waitUntilTextExists(".workspace .name a", "test-workspace"); - }); - - it("adds cluster in default workspace", async () => { - await addMinikubeCluster(app); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); - }); - - it("adds cluster in test-workspace", async () => { - await app.client.click("#current-workspace .Icon"); - await app.client.waitForVisible('.WorkspaceMenu li[title="test description"]'); - await app.client.click('.WorkspaceMenu li[title="test description"]'); - await addMinikubeCluster(app); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - }); - - it("checks if default workspace has active cluster", async () => { - await app.client.click("#current-workspace .Icon"); - await app.client.waitForVisible(".WorkspaceMenu > li:first-of-type"); - await app.client.click(".WorkspaceMenu > li:first-of-type"); - await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); - }); - }); - - const addMinikubeCluster = async (app: Application) => { - await app.client.click("div.add-cluster"); - await app.client.waitUntilTextExists("div", "Select kubeconfig file"); - await app.client.click("div.Select__control"); // show the context drop-down list - await app.client.waitUntilTextExists("div", "minikube"); - - if (!await app.client.$("button.primary").isEnabled()) { - await app.client.click("div.minikube"); // select minikube context - } // else the only context, which must be 'minikube', is automatically selected - await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) - await app.client.click("button.primary"); // add minikube cluster - }; - const waitForMinikubeDashboard = async (app: Application) => { - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.frame("minikube"); - await app.client.waitUntilTextExists("span.link-text", "Cluster"); - }; - - utils.describeIf(ready)("cluster tests", () => { - let clusterAdded = false; - const addCluster = async () => { - await clickWhatsNew(app); - await addMinikubeCluster(app); - await waitForMinikubeDashboard(app); - await app.client.click('a[href="/nodes"]'); - await app.client.waitUntilTextExists("div.TableCell", "Ready"); - }; - - describe("cluster add", () => { - beforeAll(appStart, 20000); - - afterAll(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it("allows to add a cluster", async () => { - await addCluster(); - clusterAdded = true; - }); - }); - - const appStartAddCluster = async () => { - if (clusterAdded) { - await appStart(); - await addCluster(); - } - }; - - describe("cluster pages", () => { - - beforeAll(appStartAddCluster, 40000); - - afterAll(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - const tests: { - drawer?: string - drawerId?: string - pages: { - name: string, - href: string, - expectedSelector: string, - expectedText: string - }[] - }[] = [{ - drawer: "", - drawerId: "", - pages: [{ - name: "Cluster", - href: "cluster", - expectedSelector: "div.ClusterOverview div.label", - expectedText: "Master" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Nodes", - href: "nodes", - expectedSelector: "h5.title", - expectedText: "Nodes" - }] - }, - { - drawer: "Workloads", - drawerId: "workloads", - pages: [{ - name: "Overview", - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - }, - { - name: "Pods", - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - }, - { - name: "Deployments", - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - }, - { - name: "DaemonSets", - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - }, - { - name: "StatefulSets", - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - }, - { - name: "ReplicaSets", - href: "replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets" - }, - { - name: "Jobs", - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - }, - { - name: "CronJobs", - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - }] - }, - { - drawer: "Configuration", - drawerId: "config", - pages: [{ - name: "ConfigMaps", - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - }, - { - name: "Secrets", - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - }, - { - name: "Resource Quotas", - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - }, - { - name: "Limit Ranges", - href: "limitranges", - expectedSelector: "h5.title", - expectedText: "Limit Ranges" - }, - { - name: "HPA", - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - }, - { - name: "Pod Disruption Budgets", - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - }] - }, - { - drawer: "Network", - drawerId: "networks", - pages: [{ - name: "Services", - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - }, - { - name: "Endpoints", - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - }, - { - name: "Ingresses", - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - }, - { - name: "Network Policies", - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - }] - }, - { - drawer: "Storage", - drawerId: "storage", - pages: [{ - name: "Persistent Volume Claims", - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - }, - { - name: "Persistent Volumes", - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - }, - { - name: "Storage Classes", - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Namespaces", - href: "namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Events", - href: "events", - expectedSelector: "h5.title", - expectedText: "Events" - }] - }, - { - drawer: "Apps", - drawerId: "apps", - pages: [{ - name: "Charts", - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - expectedText: "" - }, - { - name: "Releases", - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - }] - }, - { - drawer: "Access Control", - drawerId: "users", - pages: [{ - name: "Service Accounts", - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - }, - { - name: "Role Bindings", - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - }, - { - name: "Roles", - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - }, - { - name: "Pod Security Policies", - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - }] - }, - { - drawer: "Custom Resources", - drawerId: "custom-resources", - pages: [{ - name: "Definitions", - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - }] - }]; - - tests.forEach(({ drawer = "", drawerId = "", pages }) => { - if (drawer !== "") { - it(`shows ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name); - }); - } - pages.forEach(({ name, href, expectedSelector, expectedText }) => { - it(`shows ${drawer}->${name} page`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`a[href^="/${href}"]`); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); - }); - - if (drawer !== "") { - // hide the drawer - it(`hides ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow(); - }); - } - }); - }); - - describe("viewing pod logs", () => { - beforeEach(appStartAddCluster, 40000); - - afterEach(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it(`shows a logs for a pod`, async () => { - expect(clusterAdded).toBe(true); - // Go to Pods page - await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); - // Open logs tab in dock - await app.client.click(".list .TableRow:first-child"); - await app.client.waitForVisible(".Drawer"); - await app.client.click(".drawer-title .Menu li:nth-child(2)"); - // Check if controls are available - await app.client.waitForVisible(".Logs .VirtualList"); - await app.client.waitForVisible(".LogResourceSelector"); - await app.client.waitForVisible(".LogResourceSelector .SearchInput"); - await app.client.waitForVisible(".LogResourceSelector .SearchInput input"); - // Search for semicolon - await app.client.keys(":"); - await app.client.waitForVisible(".Logs .list span.active"); - // Click through controls - await app.client.click(".LogControls .show-timestamps"); - await app.client.click(".LogControls .show-previous"); - }); - }); - - describe("cluster operations", () => { - beforeEach(appStartAddCluster, 40000); - - afterEach(async () => { - if (app && app.isRunning()) { - return utils.tearDown(app); - } - }); - - it("shows default namespace", async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - }); - - it(`creates ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - await app.client.click("button.add-button"); - await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); - await app.client.keys(`${TEST_NAMESPACE}\n`); - await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); - }); - - it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); - await app.client.click(".Icon.new-dock-tab"); - await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); - await app.client.click("li.MenuItem.create-resource-tab"); - await app.client.waitForVisible(".CreateResource div.ace_content"); - // Write pod manifest to editor - await app.client.keys("apiVersion: v1\n"); - await app.client.keys("kind: Pod\n"); - await app.client.keys("metadata:\n"); - await app.client.keys(" name: nginx-create-pod-test\n"); - await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); - await app.client.keys(`${BACKSPACE}spec:\n`); - await app.client.keys(" containers:\n"); - await app.client.keys("- name: nginx-create-pod-test\n"); - await app.client.keys(" image: nginx:alpine\n"); - // Create deployment - await app.client.waitForEnabled("button.Button=Create & Close"); - await app.client.click("button.Button=Create & Close"); - // Wait until first bits of pod appears on dashboard - await app.client.waitForExist(".name=nginx-create-pod-test"); - // Open pod details - await app.client.click(".name=nginx-create-pod-test"); - await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); - }); - }); - }); }); diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts new file mode 100644 index 0000000000..3882755f4b --- /dev/null +++ b/integration/__tests__/cluster-pages.tests.ts @@ -0,0 +1,431 @@ +/* + Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE + namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the + TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube + cluster and vice versa. +*/ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; +import { addMinikubeCluster, minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube"; +import { exec } from "child_process"; +import * as util from "util"; + +export const promiseExec = util.promisify(exec); + +jest.setTimeout(60000); + +// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) +describe("Lens cluster pages", () => { + const TEST_NAMESPACE = "integration-tests"; + const BACKSPACE = "\uE003"; + let app: Application; + const ready = minikubeReady(TEST_NAMESPACE); + + utils.describeIf(ready)("test common pages", () => { + let clusterAdded = false; + const addCluster = async () => { + await utils.clickWhatsNew(app); + await addMinikubeCluster(app); + await waitForMinikubeDashboard(app); + await app.client.click('a[href="/nodes"]'); + await app.client.waitUntilTextExists("div.TableCell", "Ready"); + }; + + describe("cluster add", () => { + beforeAll(async () => app = await utils.appStart(), 20000); + + afterAll(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + it("allows to add a cluster", async () => { + await addCluster(); + clusterAdded = true; + }); + }); + + const appStartAddCluster = async () => { + if (clusterAdded) { + app = await utils.appStart(); + await addCluster(); + } + }; + + describe("cluster pages", () => { + + beforeAll(appStartAddCluster, 40000); + + afterAll(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + const tests: { + drawer?: string + drawerId?: string + pages: { + name: string, + href: string, + expectedSelector: string, + expectedText: string + }[] + }[] = [{ + drawer: "", + drawerId: "", + pages: [{ + name: "Cluster", + href: "cluster", + expectedSelector: "div.ClusterOverview div.label", + expectedText: "Master" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Nodes", + href: "nodes", + expectedSelector: "h5.title", + expectedText: "Nodes" + }] + }, + { + drawer: "Workloads", + drawerId: "workloads", + pages: [{ + name: "Overview", + href: "workloads", + expectedSelector: "h5.box", + expectedText: "Overview" + }, + { + name: "Pods", + href: "pods", + expectedSelector: "h5.title", + expectedText: "Pods" + }, + { + name: "Deployments", + href: "deployments", + expectedSelector: "h5.title", + expectedText: "Deployments" + }, + { + name: "DaemonSets", + href: "daemonsets", + expectedSelector: "h5.title", + expectedText: "Daemon Sets" + }, + { + name: "StatefulSets", + href: "statefulsets", + expectedSelector: "h5.title", + expectedText: "Stateful Sets" + }, + { + name: "ReplicaSets", + href: "replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, + { + name: "Jobs", + href: "jobs", + expectedSelector: "h5.title", + expectedText: "Jobs" + }, + { + name: "CronJobs", + href: "cronjobs", + expectedSelector: "h5.title", + expectedText: "Cron Jobs" + }] + }, + { + drawer: "Configuration", + drawerId: "config", + pages: [{ + name: "ConfigMaps", + href: "configmaps", + expectedSelector: "h5.title", + expectedText: "Config Maps" + }, + { + name: "Secrets", + href: "secrets", + expectedSelector: "h5.title", + expectedText: "Secrets" + }, + { + name: "Resource Quotas", + href: "resourcequotas", + expectedSelector: "h5.title", + expectedText: "Resource Quotas" + }, + { + name: "Limit Ranges", + href: "limitranges", + expectedSelector: "h5.title", + expectedText: "Limit Ranges" + }, + { + name: "HPA", + href: "hpa", + expectedSelector: "h5.title", + expectedText: "Horizontal Pod Autoscalers" + }, + { + name: "Pod Disruption Budgets", + href: "poddisruptionbudgets", + expectedSelector: "h5.title", + expectedText: "Pod Disruption Budgets" + }] + }, + { + drawer: "Network", + drawerId: "networks", + pages: [{ + name: "Services", + href: "services", + expectedSelector: "h5.title", + expectedText: "Services" + }, + { + name: "Endpoints", + href: "endpoints", + expectedSelector: "h5.title", + expectedText: "Endpoints" + }, + { + name: "Ingresses", + href: "ingresses", + expectedSelector: "h5.title", + expectedText: "Ingresses" + }, + { + name: "Network Policies", + href: "network-policies", + expectedSelector: "h5.title", + expectedText: "Network Policies" + }] + }, + { + drawer: "Storage", + drawerId: "storage", + pages: [{ + name: "Persistent Volume Claims", + href: "persistent-volume-claims", + expectedSelector: "h5.title", + expectedText: "Persistent Volume Claims" + }, + { + name: "Persistent Volumes", + href: "persistent-volumes", + expectedSelector: "h5.title", + expectedText: "Persistent Volumes" + }, + { + name: "Storage Classes", + href: "storage-classes", + expectedSelector: "h5.title", + expectedText: "Storage Classes" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Namespaces", + href: "namespaces", + expectedSelector: "h5.title", + expectedText: "Namespaces" + }] + }, + { + drawer: "", + drawerId: "", + pages: [{ + name: "Events", + href: "events", + expectedSelector: "h5.title", + expectedText: "Events" + }] + }, + { + drawer: "Apps", + drawerId: "apps", + pages: [{ + name: "Charts", + href: "apps/charts", + expectedSelector: "div.HelmCharts input", + expectedText: "" + }, + { + name: "Releases", + href: "apps/releases", + expectedSelector: "h5.title", + expectedText: "Releases" + }] + }, + { + drawer: "Access Control", + drawerId: "users", + pages: [{ + name: "Service Accounts", + href: "service-accounts", + expectedSelector: "h5.title", + expectedText: "Service Accounts" + }, + { + name: "Role Bindings", + href: "role-bindings", + expectedSelector: "h5.title", + expectedText: "Role Bindings" + }, + { + name: "Roles", + href: "roles", + expectedSelector: "h5.title", + expectedText: "Roles" + }, + { + name: "Pod Security Policies", + href: "pod-security-policies", + expectedSelector: "h5.title", + expectedText: "Pod Security Policies" + }] + }, + { + drawer: "Custom Resources", + drawerId: "custom-resources", + pages: [{ + name: "Definitions", + href: "crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }] + }]; + + tests.forEach(({ drawer = "", drawerId = "", pages }) => { + if (drawer !== "") { + it(`shows ${drawer} drawer`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); + await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name); + }); + } + pages.forEach(({ name, href, expectedSelector, expectedText }) => { + it(`shows ${drawer}->${name} page`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`a[href^="/${href}"]`); + await app.client.waitUntilTextExists(expectedSelector, expectedText); + }); + }); + + if (drawer !== "") { + // hide the drawer + it(`hides ${drawer} drawer`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); + await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow(); + }); + } + }); + }); + + describe("viewing pod logs", () => { + beforeEach(appStartAddCluster, 40000); + + afterEach(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + it(`shows a logs for a pod`, async () => { + expect(clusterAdded).toBe(true); + // Go to Pods page + await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); + await app.client.click('a[href^="/pods"]'); + await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + // Open logs tab in dock + await app.client.click(".list .TableRow:first-child"); + await app.client.waitForVisible(".Drawer"); + await app.client.click(".drawer-title .Menu li:nth-child(2)"); + // Check if controls are available + await app.client.waitForVisible(".Logs .VirtualList"); + await app.client.waitForVisible(".LogResourceSelector"); + await app.client.waitForVisible(".LogResourceSelector .SearchInput"); + await app.client.waitForVisible(".LogResourceSelector .SearchInput input"); + // Search for semicolon + await app.client.keys(":"); + await app.client.waitForVisible(".Logs .list span.active"); + // Click through controls + await app.client.click(".LogControls .show-timestamps"); + await app.client.click(".LogControls .show-previous"); + }); + }); + + describe("cluster operations", () => { + beforeEach(appStartAddCluster, 40000); + + afterEach(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + it("shows default namespace", async () => { + expect(clusterAdded).toBe(true); + await app.client.click('a[href="/namespaces"]'); + await app.client.waitUntilTextExists("div.TableCell", "default"); + await app.client.waitUntilTextExists("div.TableCell", "kube-system"); + }); + + it(`creates ${TEST_NAMESPACE} namespace`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click('a[href="/namespaces"]'); + await app.client.waitUntilTextExists("div.TableCell", "default"); + await app.client.waitUntilTextExists("div.TableCell", "kube-system"); + await app.client.click("button.add-button"); + await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); + await app.client.keys(`${TEST_NAMESPACE}\n`); + await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); + }); + + it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { + expect(clusterAdded).toBe(true); + await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text"); + await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); + await app.client.click('a[href^="/pods"]'); + await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + await app.client.click(".Icon.new-dock-tab"); + await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); + await app.client.click("li.MenuItem.create-resource-tab"); + await app.client.waitForVisible(".CreateResource div.ace_content"); + // Write pod manifest to editor + await app.client.keys("apiVersion: v1\n"); + await app.client.keys("kind: Pod\n"); + await app.client.keys("metadata:\n"); + await app.client.keys(" name: nginx-create-pod-test\n"); + await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); + await app.client.keys(`${BACKSPACE}spec:\n`); + await app.client.keys(" containers:\n"); + await app.client.keys("- name: nginx-create-pod-test\n"); + await app.client.keys(" image: nginx:alpine\n"); + // Create deployment + await app.client.waitForEnabled("button.Button=Create & Close"); + await app.client.click("button.Button=Create & Close"); + // Wait until first bits of pod appears on dashboard + await app.client.waitForExist(".name=nginx-create-pod-test"); + // Open pod details + await app.client.click(".name=nginx-create-pod-test"); + await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); + }); + }); + }); +}); diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts new file mode 100644 index 0000000000..ae0495673f --- /dev/null +++ b/integration/__tests__/command-palette.tests.ts @@ -0,0 +1,36 @@ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; +import { isMac } from "../../src/common/vars"; + +jest.setTimeout(60000); + +describe("Lens command palette", () => { + let app: Application; + + describe("menu", () => { + beforeAll(async () => { + app = await utils.appStart(); + await utils.clickWhatsNew(app); + }, 20000); + + afterAll(async () => { + if (app?.isRunning()) { + await utils.tearDown(app); + } + }); + + it("opens command dialog from menu", async () => { + await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette..."); + await app.client.waitUntilTextExists(".Select__option", "Preferences: Open"); + await app.client.keys("Escape"); + }); + + utils.describeIf(!isMac)("Linux & Windows", () => { + it("opens command dialog via keyboard", async () => { + await app.client.keys(["Control", "Shift", "p"]); + await app.client.waitUntilTextExists(".Select__option", "Preferences: Open"); + await app.client.keys("Escape"); + }); + }); + }); +}); diff --git a/integration/__tests__/workspace.tests.ts b/integration/__tests__/workspace.tests.ts new file mode 100644 index 0000000000..da614b0cfb --- /dev/null +++ b/integration/__tests__/workspace.tests.ts @@ -0,0 +1,58 @@ +import { Application } from "spectron"; +import * as utils from "../helpers/utils"; +import { addMinikubeCluster, minikubeReady } from "../helpers/minikube"; +import { exec } from "child_process"; +import * as util from "util"; + +export const promiseExec = util.promisify(exec); + +jest.setTimeout(60000); + +describe("Lens integration tests", () => { + let app: Application; + const ready = minikubeReady("workspace-int-tests"); + + utils.describeIf(ready)("workspaces", () => { + beforeAll(async () => { + app = await utils.appStart(); + await utils.clickWhatsNew(app); + }, 20000); + + afterAll(async () => { + if (app && app.isRunning()) { + return utils.tearDown(app); + } + }); + + const switchToWorkspace = async (name: string) => { + await app.client.click("[data-test-id=current-workspace]"); + await app.client.keys(name); + await app.client.keys("Enter"); + await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", name); + }; + + it("creates new workspace", async () => { + await app.client.click("[data-test-id=current-workspace]"); + await app.client.keys("add workspace"); + await app.client.keys("Enter"); + await app.client.keys("test-workspace"); + await app.client.keys("Enter"); + await app.client.waitUntilTextExists("[data-test-id=current-workspace-name]", "test-workspace"); + }); + + it("adds cluster in default workspace", async () => { + await switchToWorkspace("default"); + await addMinikubeCluster(app); + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active"); + }); + + it("adds cluster in test-workspace", async () => { + await switchToWorkspace("test-workspace"); + await addMinikubeCluster(app); + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + }); + }); +}); diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts new file mode 100644 index 0000000000..67ef0145d5 --- /dev/null +++ b/integration/helpers/minikube.ts @@ -0,0 +1,59 @@ +import { spawnSync } from "child_process"; +import { Application } from "spectron"; + +export function minikubeReady(testNamespace: string): boolean { + // determine if minikube is running + { + const { status } = spawnSync("minikube status", { shell: true }); + + if (status !== 0) { + console.warn("minikube not running"); + + return false; + } + } + + // Remove TEST_NAMESPACE if it already exists + { + const { status } = spawnSync(`minikube kubectl -- get namespace ${testNamespace}`, { shell: true }); + + if (status === 0) { + console.warn(`Removing existing ${testNamespace} namespace`); + + const { status, stdout, stderr } = spawnSync( + `minikube kubectl -- delete namespace ${testNamespace}`, + { shell: true }, + ); + + if (status !== 0) { + console.warn(`Error removing ${testNamespace} namespace: ${stderr.toString()}`); + + return false; + } + + console.log(stdout.toString()); + } + } + + return true; +} + +export async function addMinikubeCluster(app: Application) { + await app.client.click("div.add-cluster"); + await app.client.waitUntilTextExists("div", "Select kubeconfig file"); + await app.client.click("div.Select__control"); // show the context drop-down list + await app.client.waitUntilTextExists("div", "minikube"); + + if (!await app.client.$("button.primary").isEnabled()) { + await app.client.click("div.minikube"); // select minikube context + } // else the only context, which must be 'minikube', is automatically selected + await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) + await app.client.click("button.primary"); // add minikube cluster +} + +export async function waitForMinikubeDashboard(app: Application) { + await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); + await app.client.waitForExist(`iframe[name="minikube"]`); + await app.client.frame("minikube"); + await app.client.waitUntilTextExists("span.link-text", "Cluster"); +} diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index f445a9ae48..2cb4ce9e10 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -26,6 +26,28 @@ export function setup(): Application { }); } +export const keys = { + backspace: "\uE003" +}; + +export async function appStart() { + const app = setup(); + + await app.start(); + // Wait for splash screen to be closed + while (await app.client.getWindowCount() > 1); + await app.client.windowByIndex(0); + await app.client.waitUntilWindowLoaded(); + + return app; +} + +export async function clickWhatsNew(app: Application) { + await app.client.waitUntilTextExists("h1", "What's new?"); + await app.client.click("button.primary"); + await app.client.waitUntilTextExists("h1", "Welcome"); +} + type AsyncPidGetter = () => Promise; export async function tearDown(app: Application) { diff --git a/src/common/ipc.ts b/src/common/ipc.ts index f13ea30b9f..cb4da6ca95 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -17,33 +17,34 @@ export async function requestMain(channel: string, ...args: any[]) { return ipcRenderer.invoke(channel, ...args); } -async function getSubFrames(): Promise { +async function getSubFrames(processId: number): Promise { const subFrames: ClusterFrameInfo[] = []; clusterFrameMap.forEach(frameInfo => { subFrames.push(frameInfo); }); - return subFrames; + return subFrames.filter(frame => frame.processId === processId); } export async function broadcastMessage(channel: string, ...args: any[]) { - let subFrames: ClusterFrameInfo[]; - - if (ipcRenderer) { - subFrames = await requestMain(subFramesChannel); - } else { - subFrames = await getSubFrames(); - } const views = (webContents || remote?.webContents)?.getAllWebContents(); if (!views) return; - views.forEach(webContent => { + views.forEach(async webContent => { const type = webContent.getType(); logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); webContent.send(channel, ...args); + + let subFrames: ClusterFrameInfo[]; + + if (ipcRenderer) { + subFrames = await requestMain(subFramesChannel, webContent.getProcessId()); + } else { + subFrames = await getSubFrames(webContent.getProcessId()); + } subFrames.map((frameInfo) => { webContent.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); }); @@ -83,7 +84,7 @@ export function unsubscribeAllFromBroadcast(channel: string) { } export function bindBroadcastHandlers() { - handleRequest(subFramesChannel, async () => { - return toJS(await getSubFrames(), { recurseEverything: true }); + handleRequest(subFramesChannel, async (processId: number) => { + return toJS(await getSubFrames(processId), { recurseEverything: true }); }); } diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 921a9126a9..5c9ef6f3b4 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -243,6 +243,7 @@ export class WorkspaceStore extends BaseStore { throw new Error(`workspace ${id} doesn't exist`); } this.currentWorkspaceId = id; + clusterStore.setActive(null); } @action diff --git a/src/renderer/components/+workspaces/add-workspace.tsx b/src/renderer/components/+workspaces/add-workspace.tsx index 3491a05e7e..3d2724476e 100644 --- a/src/renderer/components/+workspaces/add-workspace.tsx +++ b/src/renderer/components/+workspaces/add-workspace.tsx @@ -36,6 +36,7 @@ export class AddWorkspace extends React.Component { placeholder="Workspace name" autoFocus={true} theme="round-black" + data-test-id="command-palette-workspace-add-name" validators={[uniqueWorkspaceName]} onSubmit={(v) => this.onSubmit(v)} dirty={true} diff --git a/src/renderer/components/+workspaces/remove-workspace.tsx b/src/renderer/components/+workspaces/remove-workspace.tsx index 7e7fc4c6d4..e2ca951efe 100644 --- a/src/renderer/components/+workspaces/remove-workspace.tsx +++ b/src/renderer/components/+workspaces/remove-workspace.tsx @@ -58,6 +58,7 @@ export class RemoveWorkspace extends React.Component { options={this.options} autoFocus={true} escapeClearsValue={false} + data-test-id="command-palette-workspace-remove-select" placeholder="Remove workspace" /> ); } diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx index 28d1f83106..55d554ec97 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.tsx @@ -17,9 +17,9 @@ export class BottomBar extends React.Component { return (
-
CommandOverlay.open()}> +
CommandOverlay.open()}> - {currentWorkspace.name} + {currentWorkspace.name}
{Array.isArray(items) && items.map(({ item }, index) => { diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index ac9c3cb358..4f27784a3b 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -74,6 +74,7 @@ export class CommandDialog extends React.Component { options={this.options} autoFocus={true} escapeClearsValue={false} + data-test-id="command-palette-search" placeholder="" /> ); }