1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Get terminal tests to pass again

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-01-10 11:31:23 -05:00
parent 7971755e18
commit c658a0b0da
15 changed files with 155 additions and 29 deletions

View File

@ -20,6 +20,5 @@ export const testUsingFakeTime = (dateTime = "2015-10-21T07:28:00Z") => {
usingFakeTime = true;
jest.useFakeTimers();
jest.setSystemTime(new Date(dateTime));
};

View File

@ -6,6 +6,44 @@ exports[`test for opening terminal tab within cluster frame when new terminal ta
<div
id="terminal-init"
/>
<div
class="Animate slide-right Drawer KubeObjectDetails flex column right leave"
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="drawer-wrapper flex column"
>
<div
class="drawer-title flex align-center"
>
<div
class="drawer-title-text flex gaps align-center"
>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
<div>
Close
</div>
</div>
<div
class="drawer-content flex column box grow"
/>
</div>
<div
class="ResizingAnchor horizontal leading"
/>
</div>
<div
class="Notifications flex column align-flex-end"
/>

View File

@ -18,6 +18,8 @@ import type { ApplicationBuilder } from "../test-utils/application-builder";
import { setupInitializingApplicationBuilder } from "../test-utils/application-builder";
import type { FindByTextWithMarkup } from "../../test-utils/findByTextWithMarkup";
import { findByTextWithMarkupFor } from "../../test-utils/findByTextWithMarkup";
import terminalStoreInjectable from "../../renderer/components/dock/terminal/store.injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
describe("test for opening terminal tab within cluster frame", () => {
let builder: ApplicationBuilder;
@ -113,8 +115,21 @@ describe("test for opening terminal tab within cluster frame", () => {
});
describe("when the next data is sent", () => {
beforeEach(() => {
beforeEach(async () => {
const terminalStore = builder.applicationWindow.only.di.inject(terminalStoreInjectable);
const terminalApi = terminalStore.getTerminalApi("terminal");
assert(terminalApi);
const waitForData = new Promise<void>(resolve => terminalApi.on("data", (message) => {
if (message) {
resolve();
}
}));
await flushPromises();
sendData("I am a prompt");
await waitForData;
});
it("renders the new data", async () => {

View File

@ -81,9 +81,10 @@ import { runManySyncFor } from "../../common/runnable/run-many-sync-for";
import type { MemoryHistory } from "history";
import { object } from "../../common/utils";
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
import { testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import createVersionDetectorInjectable from "../../main/cluster-detectors/create-version-detector.injectable";
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
import getElementByIdInjectable from "../../renderer/utils/get-element-by-id.injectable";
import { testUsingFakeTime } from "../../common/test-utils/use-fake-time";
type Callback = (di: DiContainer) => void | Promise<void>;
@ -237,8 +238,8 @@ export const setupInitializingApplicationBuilder = (init: (builder: ApplicationB
const createElectronWindowFake: CreateElectronWindow = (configuration) => {
const windowId = configuration.id;
const windowDi = getRendererDi({ doGeneralOverrides: true });
let rendered: RenderResult;
overrideForWindow(windowDi, windowId);
overrideFsWithFakes(windowDi);
@ -258,7 +259,15 @@ export const setupInitializingApplicationBuilder = (init: (builder: ApplicationB
return computed(() => [...rendererExtensionState.values()]);
});
let rendered: RenderResult;
windowDi.override(getElementByIdInjectable, () => (id) => {
const elem = rendered?.container.querySelector(`#${id}`);
if (!elem) {
throw new Error(`Missing #${id} in DOM`);
}
return elem;
});
windowHelpers.set(windowId, { di: windowDi, getRendered: () => rendered });
@ -357,7 +366,7 @@ export const setupInitializingApplicationBuilder = (init: (builder: ApplicationB
windowDi.override(currentLocationInjectable, () => ({
hostname: "localhost",
port: `${mainDi.inject(lensProxyPortInjectable).get()}`,
protocol: "http",
protocol: "https",
}));
});

View File

@ -5,7 +5,7 @@
import waitUntilPortIsUsedInjectable from "../../common/utils/wait-until-port-is-used/wait-until-port-is-used.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { ChildProcess } from "child_process";
import { Kubectl } from "../kubectl/kubectl";
import type { DeepMockProxy } from "jest-mock-extended";

View File

@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiKubePrefix } from "../common/vars";
import type { Cluster } from "../common/cluster/cluster";
import { getInjectable } from "@ogre-tools/injectable";
import type { RequestInit } from "node-fetch";
@ -23,8 +22,7 @@ const k8sRequestInjectable = getInjectable({
return async (cluster, path, { timeout = 30_000, ...init } = {}) => {
const controller = withTimeout(timeout);
const response = await lensFetch(`/${cluster.id}${apiKubePrefix}${path}`, {
const response = await lensFetch(`/${cluster.id}${path}`, {
...init,
signal: controller.signal,
});

View File

@ -17,7 +17,7 @@ import type { SetRequired } from "type-fest";
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
import type { Logger } from "../../common/logger";
import type { SelfSignedCert } from "selfsigned";
import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable";
import type { GetClusterForRequest } from "./get-cluster-for-request.injectable";
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
export type ProxyRequestHandler = (args: ProxyApiRequestArgs) => void | Promise<void>;

View File

@ -11,17 +11,19 @@ import currentLocationInjectable from "./current-location.injectable";
import defaultWebsocketApiParamsInjectable from "./default-websocket-api-params.injectable";
import type { TerminalApiDependencies, TerminalApiQuery } from "./terminal-api";
import { TerminalApi } from "./terminal-api";
import websocketAgentInjectable from "./websocket-agent.injectable";
export type CreateTerminalApi = (query: TerminalApiQuery) => TerminalApi;
const createTerminalApiInjectable = getInjectable({
id: "create-terminal-api",
instantiate: (di): CreateTerminalApi => {
const partialDeps = {
const partialDeps: Omit<TerminalApiDependencies, "hostedClusterId"> = {
requestShellApiToken: di.inject(requestShellApiTokenInjectable),
defaultParams: di.inject(defaultWebsocketApiParamsInjectable),
logger: di.inject(loggerInjectable),
currentLocation: di.inject(currentLocationInjectable),
websocketAgent: di.inject(websocketAgentInjectable),
};
return (query) => {
@ -29,12 +31,10 @@ const createTerminalApiInjectable = getInjectable({
assert(hostedClusterId, "Can only create Terminal APIs within cluster frames");
const deps: TerminalApiDependencies = {
return new TerminalApi({
...partialDeps,
hostedClusterId,
};
return new TerminalApi(deps, query);
}, query);
};
},
});

View File

@ -13,6 +13,7 @@ import { type TerminalMessage, TerminalChannels } from "../../common/terminal/ch
import type { RequestShellApiToken } from "../../features/terminal/renderer/request-shell-api-token.injectable";
import type { CurrentLocation } from "./current-location.injectable";
import type { ClusterId } from "../../common/cluster-types";
import type { CloseEvent, Event, MessageEvent } from "ws";
enum TerminalColor {
RED = "\u001b[31m",
@ -127,7 +128,9 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
}
}
protected _onMessage({ data, ...evt }: MessageEvent<string>): void {
protected _onMessage(event: MessageEvent): void {
const data = event.data as string;
try {
const message = JSON.parse(data) as TerminalMessage;
@ -138,7 +141,7 @@ export class TerminalApi extends WebSocketApi<TerminalEvents> {
* don't want this data to survive if the app is closed
*/
window.localStorage.setItem(`${this.query.id}:last-data`, message.data);
super._onMessage({ data: message.data, ...evt });
super._onMessage({ ...event, data: message.data });
break;
case TerminalChannels.CONNECTED:
this.emit("connected");

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Agent } from "https";
import { getGlobalOverride } from "../../common/test-utils/get-global-override";
import websocketAgentInjectable from "./websocket-agent.injectable";
export default getGlobalOverride(websocketAgentInjectable, () => new Agent({
rejectUnauthorized: false,
}));

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { Agent } from "https";
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
const websocketAgentInjectable = getInjectable({
id: "websocket-agent",
instantiate: (di) => {
const lensProxyCertificate = di.inject(lensProxyCertificateInjectable);
return new Agent({
cert: lensProxyCertificate.get().cert,
});
},
});
export default websocketAgentInjectable;

View File

@ -9,6 +9,9 @@ import type TypedEventEmitter from "typed-emitter";
import type { Defaulted } from "../utils";
import type { DefaultWebsocketApiParams } from "./default-websocket-api-params.injectable";
import type { Logger } from "../../common/logger";
import type { CloseEvent, Event, MessageEvent } from "ws";
import { WebSocket } from "ws";
import type { Agent } from "https";
interface WebsocketApiParams {
/**
@ -66,6 +69,7 @@ export interface WebSocketEvents {
export interface WebSocketApiDependencies {
readonly defaultParams: DefaultWebsocketApiParams;
readonly logger: Logger;
readonly websocketAgent: Agent;
}
export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter as { new<T>(): TypedEventEmitter<T> })<Events> {
@ -100,7 +104,9 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
this.socket?.close();
// start new connection
this.socket = new WebSocket(url);
this.socket = new WebSocket(url, {
agent: this.dependencies.websocketAgent,
});
this.socket.addEventListener("open", ev => this._onOpen(ev));
this.socket.addEventListener("message", ev => this._onMessage(ev));
this.socket.addEventListener("error", ev => this._onError(ev));
@ -164,7 +170,9 @@ export class WebSocketApi<Events extends WebSocketEvents> extends (EventEmitter
this.writeLog("%cOPEN", "color:green;font-weight:bold;", evt);
}
protected _onMessage({ data }: MessageEvent<string>): void {
protected _onMessage(event: MessageEvent): void {
const data = event.data as string;
(this as TypedEventEmitter<WebSocketEvents>).emit("data", data);
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
}

View File

@ -3,16 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { memoize } from "lodash";
import getElementByIdInjectable from "../../../utils/get-element-by-id.injectable";
/**
* It is necessary to have this a function because in a testing environment the DOM isn't
* available until after first render
*/
const terminalSpawningPoolInjectable = getInjectable({
id: "terminal-spawning-pool",
instantiate: () => {
const pool = document.getElementById("terminal-init");
instantiate: (di) => {
const getElementById = di.inject(getElementByIdInjectable);
assert(pool, "DOM MUST contain #terminal-init element");
return pool;
return memoize(() => getElementById("terminal-init"));
},
});

View File

@ -24,12 +24,12 @@ import { SearchAddon } from "xterm-addon-search";
import { WebglAddon } from "xterm-addon-webgl";
export interface TerminalDependencies {
readonly spawningPool: HTMLElement;
readonly terminalConfig: IComputedValue<TerminalConfig>;
readonly terminalCopyOnSelect: IComputedValue<boolean>;
readonly isMac: boolean;
readonly xtermColorTheme: IComputedValue<Record<string, string>>;
readonly logger: Logger;
spawningPool: () => HTMLElement;
openLinkInBrowser: OpenLinkInBrowser;
createTerminalRenderer: CreateTerminalRenderer;
}
@ -73,7 +73,7 @@ export class Terminal {
const { elem } = this;
if (elem) {
this.dependencies.spawningPool.appendChild(elem);
this.dependencies.spawningPool().appendChild(elem);
}
}
@ -106,7 +106,7 @@ export class Terminal {
this.xterm.loadAddon(new WebLinksAddon());
this.xterm.loadAddon(new SearchAddon());
this.xterm.open(this.dependencies.spawningPool);
this.xterm.open(this.dependencies.spawningPool());
try {
const webgl = new WebglAddon();

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
const getElementByIdInjectable = getInjectable({
id: "get-element-by-id",
instantiate: () => (id: string) => {
const elem = document.getElementById(id);
if (!elem) {
throw new Error(`Missing #${id} in DOM`);
}
return elem;
},
causesSideEffects: true,
});
export default getElementByIdInjectable;