mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix unit tests and update docs
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
2544ba9e09
commit
6c4c2b714b
@ -6,11 +6,21 @@ Lens provides a routing mechanism that extensions can use to register custom han
|
||||
|
||||
## Registering A Protocol Handler
|
||||
|
||||
The method `onProtocolRequest` exists both on [`LensMainExtension`](extensions/api/classes/lensmainextension/#onprotocolrequest) and on [`LensRendererExtension`](extensions/api/classes/lensrendererextension/#onprotocolrequest).
|
||||
This is how, as an extension developer, you can register handlers for your extension.
|
||||
The field `protocolhandlers` exists both on [`LensMainExtension`](extensions/api/classes/lensmainextension/#protocolhandlers) and on [`LensRendererExtension`](extensions/api/classes/lensrendererextension/#protocolhandlers).
|
||||
This field will be iterated through every time a `lens://` request gets sent to the application.
|
||||
The `pathSchema` argument must comply with the [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) package's `compileToRegex` function.
|
||||
|
||||
Once you have registered a handler it will be called when a user opens a link on their computer.
|
||||
Handlers will be run in both `main` and `renderer` in parallel with no synchronization between the two processes.
|
||||
Furthermore, both `main` and `renderer` are routed separately.
|
||||
In other words, which handler is selected in either process is independent from the list of possible handlers in the other.
|
||||
|
||||
## Deregistering A Protocol Handler
|
||||
|
||||
All that is needed to deregister a handler is to remove it from the array of handlers.
|
||||
|
||||
## Routing Algorithm
|
||||
|
||||
The routing mechanism for extensions is quite straight forward.
|
||||
For example consider an extension `example-extension` which is published by the `@mirantis` org.
|
||||
If it were to register a handler with `"/display/:type"` as its corresponding link then we would match the following URI like this:
|
||||
@ -45,9 +55,3 @@ On the other hand, the subpath `"/display/notification"` would be routed to #3.
|
||||
|
||||
The URI is routed to the most specific matching `pathSchema`.
|
||||
This way the `"/"` (root) `pathSchema` acts as a sort of catch all or default route if no other route matches.
|
||||
|
||||
### Cleaning Up
|
||||
|
||||
Currently there is not way to remove a protocol handler once it has been registered.
|
||||
Handlers will not be called if the extension is deactivated or uninstalled.
|
||||
This means that the handlers should be added (or re-added as the case may be) on every activation of an extension instance.
|
||||
|
||||
@ -191,7 +191,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
||||
}
|
||||
|
||||
// remove the extension name from the path name so we don't need to match on it anymore
|
||||
url.set("pathname", url.pathname.slice(extension.name.length));
|
||||
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||
|
||||
const handlers = extension
|
||||
.protocolHandlers
|
||||
|
||||
@ -55,7 +55,7 @@ export class ExtensionLoader {
|
||||
@computed get userExtensionsByName(): Map<string, LensExtension> {
|
||||
const res = new Map();
|
||||
|
||||
for (const [, val] of this.instances) {
|
||||
for (const [, val] of this.instances.toJS()) {
|
||||
if (val.isBundled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@ import type { MenuRegistration } from "./registries/menu-registry";
|
||||
import { LensExtension } from "./lens-extension";
|
||||
import { WindowManager } from "../main/window-manager";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import { RouteHandler } from "../common/protocol-handler";
|
||||
import { LensProtocolRouterMain } from "../main/protocol-handler";
|
||||
|
||||
export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
@ -18,23 +16,4 @@ export class LensMainExtension extends LensExtension {
|
||||
|
||||
await windowManager.navigate(pageUrl, frameId);
|
||||
}
|
||||
|
||||
async disable() {
|
||||
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||
|
||||
lprm.removeExtensionHandlers(this.name);
|
||||
|
||||
return super.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler to be called when a `lens://` link is called.
|
||||
* @param pathSchema The path schema for the route.
|
||||
* @param handler The function to call when this route has been matched
|
||||
*/
|
||||
onProtocolRequest(pathSchema: string, handler: RouteHandler): void {
|
||||
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||
|
||||
lprm.extensionOn(this.name, pathSchema, handler);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { LensProtocolRouterMain } from "../router";
|
||||
import Url from "url-parse";
|
||||
import { noop } from "../../../common/utils";
|
||||
import { extensionsStore } from "../../../extensions/extensions-store";
|
||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||
import * as uuid from "uuid";
|
||||
import { LensMainExtension } from "../../../extensions/core-api";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
|
||||
import Url from "url-parse";
|
||||
|
||||
jest.mock("../../../common/ipc");
|
||||
|
||||
function throwIfDefined(val: any): void {
|
||||
if (val != null) {
|
||||
@ -13,14 +20,16 @@ describe("protocol router tests", () => {
|
||||
let lpr: LensProtocolRouterMain;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(extensionsStore as any).state.clear();
|
||||
(extensionLoader as any).instances.clear();
|
||||
LensProtocolRouterMain.resetInstance();
|
||||
lpr = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||
});
|
||||
|
||||
it("should throw on non-lens URLS", async () => {
|
||||
try {
|
||||
expect(await lpr.route(Url("https://google.ca"))).toBeUndefined();
|
||||
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
@ -28,134 +37,222 @@ describe("protocol router tests", () => {
|
||||
|
||||
it("should throw when host not internal or extension", async () => {
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://foobar"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not throw when has valid host", async () => {
|
||||
(extensionsStore as any).state.set("@mirantis/minikube", { enabled: true, name: "@mirantis/minikube" });
|
||||
lpr.on("/", noop);
|
||||
lpr.extensionOn("@mirantis/minikube", "/", noop);
|
||||
const extId = uuid.v4();
|
||||
const ext = new LensMainExtension({
|
||||
id: extId,
|
||||
manifestPath: "/foo/bar",
|
||||
manifest: {
|
||||
name: "@mirantis/minikube",
|
||||
version: "0.1.1",
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
ext.protocolHandlers.push({
|
||||
pathSchema: "/",
|
||||
handler: noop,
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||
|
||||
lpr.addInternalHandler("/", noop);
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://internal"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://internal")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://extension/@mirantis/minikube"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://extension/@mirantis/minikube")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
expect(broadcastMessage).toHaveBeenNthCalledWith(1, ProtocolHandlerInternal, new Url("lens://internal", true));
|
||||
expect(broadcastMessage).toHaveBeenNthCalledWith(2, ProtocolHandlerExtension, new Url("lens://extension/@mirantis/minikube", true));
|
||||
});
|
||||
|
||||
it("should call handler if matches", async () => {
|
||||
let called = false;
|
||||
|
||||
lpr.on("/page", () => { called = true; });
|
||||
lpr.addInternalHandler("/page", () => { called = true; });
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://internal/page"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://internal/page")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
expect(called).toBe(true);
|
||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, new Url("lens://internal/page", true));
|
||||
});
|
||||
|
||||
it("should call most exact handler", async () => {
|
||||
let called: any = 0;
|
||||
|
||||
lpr.on("/page", () => { called = 1; });
|
||||
lpr.on("/page/:id", params => { called = params.pathname.id; });
|
||||
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||
lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; });
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://internal/page/foo"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://internal/page/foo")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
expect(called).toBe("foo");
|
||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, new Url("lens://internal/page/foo", true));
|
||||
});
|
||||
|
||||
it("should call most exact handler for an extension", async () => {
|
||||
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||
let called: any = 0;
|
||||
|
||||
lpr.extensionOn("@foobar/icecream", "/page", () => { called = 1; });
|
||||
lpr.extensionOn("@foobar/icecream", "/page/:id", params => { called = params.pathname.id; });
|
||||
const extId = uuid.v4();
|
||||
const ext = new LensMainExtension({
|
||||
id: extId,
|
||||
manifestPath: "/foo/bar",
|
||||
manifest: {
|
||||
name: "@foobar/icecream",
|
||||
version: "0.1.1",
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
ext.protocolHandlers
|
||||
.push({
|
||||
pathSchema: "/page",
|
||||
handler: () => { called = 1; },
|
||||
}, {
|
||||
pathSchema: "/page/:id",
|
||||
handler: params => { called = params.pathname.id; },
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://extension/@foobar/icecream/page/foob"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
expect(called).toBe("foob");
|
||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, new Url("lens://extension/@foobar/icecream/page/foob", true));
|
||||
});
|
||||
|
||||
it("should work with non-org extensions", async () => {
|
||||
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||
(extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||
let called: any = 0;
|
||||
|
||||
lpr.extensionOn("icecream", "/page", () => { called = 1; });
|
||||
lpr.extensionOn("@foobar/icecream", "/page/:id", params => { called = params.pathname.id; });
|
||||
{
|
||||
const extId = uuid.v4();
|
||||
const ext = new LensMainExtension({
|
||||
id: extId,
|
||||
manifestPath: "/foo/bar",
|
||||
manifest: {
|
||||
name: "@foobar/icecream",
|
||||
version: "0.1.1",
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
ext.protocolHandlers
|
||||
.push({
|
||||
pathSchema: "/page/:id",
|
||||
handler: params => { called = params.pathname.id; },
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
}
|
||||
|
||||
{
|
||||
const extId = uuid.v4();
|
||||
const ext = new LensMainExtension({
|
||||
id: extId,
|
||||
manifestPath: "/foo/bar",
|
||||
manifest: {
|
||||
name: "icecream",
|
||||
version: "0.1.1",
|
||||
},
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
|
||||
ext.protocolHandlers
|
||||
.push({
|
||||
pathSchema: "/page",
|
||||
handler: () => { called = 1; },
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" });
|
||||
}
|
||||
|
||||
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||
(extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://extension/icecream/page"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
}
|
||||
|
||||
expect(called).toBe(1);
|
||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, new Url("lens://extension/icecream/page", true));
|
||||
});
|
||||
|
||||
it("should throw if urlSchema is invalid", () => {
|
||||
expect(() => lpr.on("/:@", noop)).toThrowError();
|
||||
expect(() => lpr.extensionOn("@foobar/icecream", "/page/:@", noop)).toThrowError();
|
||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||
});
|
||||
|
||||
it("should call most exact handler with 3 found handlers", async () => {
|
||||
let called: any = 0;
|
||||
|
||||
lpr.on("/", () => { called = 2; });
|
||||
lpr.on("/page", () => { called = 1; });
|
||||
lpr.on("/page/foo", () => { called = 3; });
|
||||
lpr.on("/page/bar", () => { called = 4; });
|
||||
lpr.addInternalHandler("/", () => { called = 2; });
|
||||
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||
lpr.addInternalHandler("/page/foo", () => { called = 3; });
|
||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://internal/page/foo/bar/bat"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://internal/page/foo/bar/bat")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
expect(called).toBe(3);
|
||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, new Url("lens://internal/page/foo/bar/bat", true));
|
||||
});
|
||||
|
||||
it("should call most exact handler with 2 found handlers", async () => {
|
||||
let called: any = 0;
|
||||
|
||||
lpr.on("/", () => { called = 2; });
|
||||
lpr.on("/page", () => { called = 1; });
|
||||
lpr.on("/page/bar", () => { called = 4; });
|
||||
lpr.addInternalHandler("/", () => { called = 2; });
|
||||
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||
lpr.addInternalHandler("/page/bar", () => { called = 4; });
|
||||
|
||||
try {
|
||||
expect(await lpr.route(Url("lens://internal/page/foo/bar/bat"))).toBeUndefined();
|
||||
expect(await lpr.route("lens://internal/page/foo/bar/bat")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(throwIfDefined(error)).not.toThrow();
|
||||
|
||||
}
|
||||
|
||||
expect(called).toBe(1);
|
||||
expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, new Url("lens://internal/page/foo/bar/bat", true));
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import logger from "../logger";
|
||||
import * as proto from "../../common/protocol-handler";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import Url from "url-parse";
|
||||
import { LensExtension } from "../../extensions/lens-extension";
|
||||
import { broadcastMessage } from "../../common/ipc";
|
||||
|
||||
export interface FallbackHandler {
|
||||
(name: string): Promise<boolean>;
|
||||
@ -70,23 +70,22 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
}
|
||||
|
||||
protected _routeToInternal(url: Url): void {
|
||||
super._routeToExtension(url);
|
||||
super._routeToInternal(url);
|
||||
|
||||
WindowManager.getInstance<WindowManager>().sendToView({
|
||||
channel: proto.ProtocolHandlerInternal,
|
||||
data: [url],
|
||||
});
|
||||
broadcastMessage(proto.ProtocolHandlerInternal, url);
|
||||
}
|
||||
|
||||
protected async _routeToExtension(url: Url): Promise<void> {
|
||||
// this needs to be done first, so that the missing extension handlers can
|
||||
// be called before notifying the renderer.
|
||||
await super._routeToExtension(url);
|
||||
/**
|
||||
* This needs to be done first, so that the missing extension handlers can
|
||||
* be called before notifying the renderer.
|
||||
*
|
||||
* Note: this needs to clone the url because _routeToExtension modifies its
|
||||
* argument.
|
||||
*/
|
||||
await super._routeToExtension(new Url(url.toString(), true));
|
||||
|
||||
WindowManager.getInstance<WindowManager>().sendToView({
|
||||
channel: proto.ProtocolHandlerExtension,
|
||||
data: [url],
|
||||
});
|
||||
broadcastMessage(proto.ProtocolHandlerExtension, url);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user