diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 4a83118e11..af029a23e9 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -2,28 +2,16 @@ import { Application } from "spectron"; import * as utils from "../helpers/utils"; import { listHelmRepositories } from "../helpers/utils"; import { fail } from "assert"; -import open from "open"; -import { AbortController } from "abort-controller"; + 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", () => { let app: Application; - let abortContoller: AbortController; describe("app start", () => { - beforeAll(async () => { - try { - app = await utils.appStart(); - } catch (error) { - fail(error); - } - }); - - beforeEach(() => { - abortContoller = new AbortController(); - }); + beforeAll(async () => app = await utils.appStart(), 20000); afterAll(async () => { if (app?.isRunning()) { @@ -31,10 +19,6 @@ describe("Lens integration tests", () => { } }); - afterEach(() => { - abortContoller.abort(); - }); - it('shows "whats new"', async () => { await utils.clickWhatsNew(app); }); @@ -44,26 +28,6 @@ describe("Lens integration tests", () => { await app.client.waitUntilTextExists("h2", "Add Cluster"); }); - /** - * skipping this for the time being until we can figure out why they are opening a second instace - * and seemingly bypassing the single instance lock - */ - describe.skip("protocol app start", () => { - it("should handle opening lens:// links", async () => { - await open("lens://app/foobar"); - - await utils.waitForLogsToContain(app, abortContoller, { - main: ["No handler", "lens://app/foobar"], - renderer: ["No handler", "lens://app/foobar"], - }); - }); - - it("should opening lens://app/preferences", async () => { - await open("lens://app/preferences"); - await app.client.waitUntilTextExists("h2", "Preferences"); - }); - }); - describe("preferences page", () => { it('shows "preferences"', async () => { const appName: string = process.platform === "darwin" ? "Lens" : "File"; diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index 45eb1ee7cc..e73774f86a 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -9,8 +9,6 @@ import * as utils from "../helpers/utils"; import { addMinikubeCluster, minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube"; import { exec } from "child_process"; import * as util from "util"; -import { delay } from "../../src/common/utils"; -import { AbortController } from "abort-controller"; export const promiseExec = util.promisify(exec); @@ -34,21 +32,11 @@ describe("Lens cluster pages", () => { }; describe("cluster add", () => { - beforeAll(async () => { - try { - app = await utils.appStart(); - } catch (error) { - fail(error); - } - }); + beforeAll(async () => app = await utils.appStart(), 20000); afterAll(async () => { - try { - if (app?.isRunning()) { - await utils.tearDown(app); - } - } catch (error) { - fail(error); + if (app && app.isRunning()) { + return utils.tearDown(app); } }); @@ -65,420 +53,299 @@ describe("Lens cluster pages", () => { } }; - describe("cluster menu pages", () => { - beforeAll(async () => { - try { - await appStartAddCluster(); - } catch (error) { - fail(error); - } - }); + describe("cluster pages", () => { + + beforeAll(appStartAddCluster, 40000); afterAll(async () => { - try { - if (app?.isRunning()) { - await utils.tearDown(app); - } - } catch (error) { - fail(error); + if (app && app.isRunning()) { + return utils.tearDown(app); } }); - type Page = [ - string, // name - { + const tests: { + drawer?: string + drawerId?: string + pages: { + name: string, href: string, expectedSelector: string, - expectedText: string, - } - ]; - - type PageTestCase = [ - string, // drawer + 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" + }, { - drawerId: string, - pages: Page[], + 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); + }); } - ]; - - const noSubMenuTests: Page[] = [ - [ - "Cluster", - { - href: "cluster", - expectedSelector: "div.ClusterOverview div.label", - expectedText: "Master", - } - ], - [ - "Nodes", - { - href: "nodes", - expectedSelector: "h5.title", - expectedText: "Nodes" - } - ], - [ - "Namespaces", - { - href: "namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces" - } - ], - [ - "Events", - { - href: "events", - expectedSelector: "h5.title", - expectedText: "Events" - } - ], - ]; - - const subMenuTests: PageTestCase[] = [ - [ - "Workloads", - { - drawerId: "workloads", - pages: [ - [ - "Overview", - { - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - } - ], - [ - "Pods", - { - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - } - ], - [ - "Deployments", - { - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - } - ], - [ - "DaemonSets", - { - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - } - ], - [ - "StatefulSets", - { - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - } - ], - [ - "ReplicaSets", - { - href: "replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets" - } - ], - [ - "Jobs", - { - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - } - ], - [ - "CronJobs", - { - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - } - ] - ] - } - ], - [ - "Configuration", - { - drawerId: "config", - pages: [ - [ - "ConfigMaps", - { - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - } - ], - [ - "Secrets", - { - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - } - ], - [ - "Resource Quotas", - { - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - } - ], - [ - "Limit Ranges", - { - href: "limitranges", - expectedSelector: "h5.title", - expectedText: "Limit Ranges" - } - ], - [ - "HPA", - { - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - } - ], - [ - "Pod Disruption Budgets", - { - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - } - ] - ] - } - ], - [ - "Network", - { - drawerId: "networks", - pages: [ - [ - "Services", - { - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - } - ], - [ - "Endpoints", - { - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - } - ], - [ - "Ingresses", - { - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - } - ], - [ - "Network Policies", - { - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - } - ] - ] - } - ], - [ - "Storage", - { - drawerId: "storage", - pages: [ - [ - "Persistent Volume Claims", - { - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - } - ], - [ - "Persistent Volumes", - { - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - } - ], - [ - "Storage Classes", - { - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - } - ] - ] - } - ], - [ - "Apps", - { - drawerId: "apps", - pages: [ - [ - "Charts", - { - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - expectedText: "" - } - ], - [ - "Releases", - { - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - } - ] - ] - } - ], - [ - "Access Control", - { - drawerId: "users", - pages: [ - [ - "Service Accounts", - { - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - } - ], - [ - "Role Bindings", - { - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - } - ], - [ - "Roles", - { - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - } - ], - [ - "Pod Security Policies", - { - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - } - ] - ] - } - ], - [ - "Custom Resources", - { - drawerId: "custom-resources", - pages: [ - [ - "Definitions", - { - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - } - ] - ] - } - ] - ]; - - describe.each(noSubMenuTests)("%s", (name, { href, expectedSelector, expectedText }) => { - it("shows page", async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`a[href^="/${href}"]`); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); - }); - - describe.each(subMenuTests)("%s Drawer", (drawer, { drawerId, pages }) => { - it("does open", 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][1].href}"]`, pages[0][0]); + 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); + }); }); - it.each(pages)("shows %s page", async (name, { href, expectedSelector, expectedText }) => { - expect(clusterAdded).toBe(true); - await app.client.click(`a[href^="/${href}"]`); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); - - it("does close", async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`); - - try { - expect(await app.client.waitUntilTextExists(`a[href^="/${pages[0][1].href}"]`, pages[0][0], 100)).toBeUndefined(); - } catch (error) { - expect(error).not.toBeUndefined(); - } - }); + 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", () => { - let abortContoller: AbortController; - - beforeEach(async () => { - abortContoller = new AbortController(); - - try { - await appStartAddCluster(); - } catch (error) { - fail(error); - } - }); + beforeEach(appStartAddCluster, 40000); afterEach(async () => { - abortContoller.abort(); - - try { - if (app?.isRunning()) { - await utils.tearDown(app); - } - } catch (error) { - fail(error); + if (app && app.isRunning()) { + return utils.tearDown(app); } }); - it("shows a logs for a pod", async () => { + 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"); @@ -488,13 +355,19 @@ describe("Lens cluster pages", () => { await app.client.keys("kube-system"); await app.client.keys("Enter");// "\uE007" await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); + let podMenuItemEnabled = false; - await utils.waitForLogsToContain(app, abortContoller, { - renderer: ["[EXTENSION]: enabled lens-pod-menu@"], - }); + // Wait until extensions are enabled on renderer + while (!podMenuItemEnabled) { + const logs = await app.client.getRenderProcessLogs(); - await delay(2000); // Give some extra time to prepare extensions + podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@")); + if (!podMenuItemEnabled) { + await new Promise(r => setTimeout(r, 1000)); + } + } + await new Promise(r => setTimeout(r, 500)); // Give some extra time to prepare extensions // Open logs tab in dock await app.client.click(".list .TableRow:first-child"); await app.client.waitForVisible(".Drawer"); @@ -514,21 +387,11 @@ describe("Lens cluster pages", () => { }); describe("cluster operations", () => { - beforeEach(async () => { - try { - await appStartAddCluster(); - } catch (error) { - fail(error); - } - }); + beforeEach(appStartAddCluster, 40000); afterEach(async () => { - try { - if (app?.isRunning()) { - await utils.tearDown(app); - } - } catch (error) { - fail(error); + if (app && app.isRunning()) { + return utils.tearDown(app); } }); diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts index 152bb142fa..789806c445 100644 --- a/integration/__tests__/command-palette.tests.ts +++ b/integration/__tests__/command-palette.tests.ts @@ -7,21 +7,11 @@ describe("Lens command palette", () => { let app: Application; describe("menu", () => { - beforeAll(async () => { - try { - app = await utils.appStart(); - } catch (error) { - fail(error); - } - }); + beforeAll(async () => app = await utils.appStart(), 20000); afterAll(async () => { - try { - if (app?.isRunning()) { - await utils.tearDown(app); - } - } catch (error) { - fail(error); + if (app?.isRunning()) { + await utils.tearDown(app); } }); diff --git a/integration/__tests__/workspace.tests.ts b/integration/__tests__/workspace.tests.ts index e23a65bf85..4164151b0f 100644 --- a/integration/__tests__/workspace.tests.ts +++ b/integration/__tests__/workspace.tests.ts @@ -14,21 +14,13 @@ describe("Lens integration tests", () => { utils.describeIf(ready)("workspaces", () => { beforeAll(async () => { - try { - app = await utils.appStart(); - await utils.clickWhatsNew(app); - } catch (error) { - fail(error); - } - }); + app = await utils.appStart(); + await utils.clickWhatsNew(app); + }, 20000); afterAll(async () => { - try { - if (app?.isRunning()) { - await utils.tearDown(app); - } - } catch (error) { - fail(error); + if (app && app.isRunning()) { + return utils.tearDown(app); } }); diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 421097d736..f96c9124e2 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -1,31 +1,12 @@ -import { AppConstructorOptions, Application } from "spectron"; +import { Application } from "spectron"; import * as util from "util"; import { exec } from "child_process"; -import { delay } from "../../src/common/utils"; -import { AbortController } from "abort-controller"; -interface AppTestingPaths { - testingPath: string, -} - -function getAppTestingPaths(): AppTestingPaths { - switch (process.platform) { - case "win32": - return { - testingPath: "./dist/win-unpacked/Lens.exe", - }; - case "linux": - return { - testingPath: "./dist/linux-unpacked/kontena-lens", - }; - case "darwin": - return { - testingPath: "./dist/mac/Lens.app/Contents/MacOS/Lens", - }; - default: - throw new TypeError(`platform ${process.platform} is not supported`); - } -} +const AppPaths: Partial> = { + "win32": "./dist/win-unpacked/Lens.exe", + "linux": "./dist/linux-unpacked/kontena-lens", + "darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", +}; export function itIf(condition: boolean) { return condition ? it : it.skip; @@ -35,18 +16,16 @@ export function describeIf(condition: boolean) { return condition ? describe : describe.skip; } -export function setup(): AppConstructorOptions { - const appPath = getAppTestingPaths(); - - return { - path: appPath.testingPath, +export function setup(): Application { + return new Application({ + path: AppPaths[process.platform], // path to electron app args: [], startTimeout: 30000, waitTimeout: 60000, env: { CICD: "true" } - }; + }); } export const keys = { @@ -54,7 +33,7 @@ export const keys = { }; export async function appStart() { - const app = new Application(setup()); + const app = setup(); await app.start(); // Wait for splash screen to be closed @@ -62,31 +41,6 @@ export async function appStart() { await app.client.windowByIndex(0); await app.client.waitUntilWindowLoaded(); - /** - * This is commented out to pass CI, need to do some more investiagation into why this isn't working - */ - // if (process.platform === "linux") { - // const testingDesktop = [ - // "[Desktop Entry]", - // "Name=Lens", - // `Exec=${path.resolve(getAppTestingPaths().testingPath)} %U`, - // "Terminal=false", - // "Type=Application", - // "Icon=lens", - // "StartupWMClass=Lens", - // "Comment=Lens - The Kubernetes IDE", - // "MimeType=x-scheme-handler/lens;", - // "Categories=Network;" - // ].join("\n"); - - // await mkdirp(path.join(os.homedir(), ".local/share/applications/")); - // await writeFile(path.join(os.homedir(), ".local/share/applications/lens-testing.desktop"), testingDesktop); - - // const { status } = spawnSync("xdg-settings set default-url-scheme-handler lens lens-testing.desktop", { shell: true }); - - // expect(status).toBe(0); - // } - return app; } @@ -132,85 +86,3 @@ export async function listHelmRepositories(retries = 0): Promise.*)", source: http:\/\//; - -export interface LogMatches { - renderer?: string[]; - main?: string[]; -} - -interface LogLines { - renderer: string[]; - main: string[]; -} - -async function* splitLogs(app: Application, signal: AbortController): AsyncGenerator { - let lastLogLineCount = 0; - - while (!signal.signal.aborted) { // infinite loop - const curLogs: string[] = (app as any).chromeDriver.getLogs(); - const newLogs = curLogs.slice(lastLogLineCount); - - lastLogLineCount = curLogs.length; - - const item: LogLines = { - renderer: [], - main: [], - }; - - for (const logLine of newLogs) { - const logParts = logLine.match(rendererLogPrefixMatcher); - - if (logParts === null) { - item.main.push(logLine); - } else { - item.renderer.push(logParts.groups.message); - } - } - - yield item; - await delay(500, signal); // only delay after the first attempt and fail fast if the signal has occured - } -} - -/** - * Wait for all of `values` to be part of the logs. Does not clear logs. Does - * not work well with `app.client.get(Main|Renderer)ProcessLogs()` - * - * Note: this is a "best attempt" since spectron's `getMainProcessLogs` sometimes - * contains `renderer` logs. - * @param app The spectron app that we are testing against - * @param source Whether to wait for renderer or main logs - * @param values The list of strings that should all be contained in the logs - */ -export async function waitForLogsToContain(app: Application, signal: AbortController, matches: LogMatches): Promise { - const notYetFound = { - main: new Set(matches.main ?? []), - renderer: new Set(matches.renderer ?? []), - }; - - for await (const logs of splitLogs(app, signal)) { - mainMatch: for (const logPart of notYetFound.main) { - for (const logLine of logs.main) { - if (logLine.includes(logPart)) { - notYetFound.main.delete(logPart); - continue mainMatch; // we have found this log part, try the next part - } - } - } - - rendererMatch: for (const logPart of notYetFound.renderer) { - for (const logLine of logs.renderer) { - if (logLine.includes(logPart)) { - notYetFound.renderer.delete(logPart); - continue rendererMatch; // we have found this log part, try the next part - } - } - } - - if (notYetFound.main.size === 0 && notYetFound.renderer.size === 0) { - return; // we are done, have found all log parts - } - } -}