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

make more injectable for testing

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-07-06 09:48:19 -04:00
parent 62af94bb2d
commit 28227c923d
30 changed files with 1349 additions and 273 deletions

View File

@ -74,8 +74,7 @@
"<rootDir>/src/extensions/npm"
],
"setupFiles": [
"<rootDir>/src/jest.setup.ts",
"jest-canvas-mock"
"<rootDir>/src/jest.setup.ts"
],
"globalSetup": "<rootDir>/src/jest.timezone.ts",
"setupFilesAfterEnv": [
@ -354,6 +353,7 @@
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"ansi_up": "^5.1.0",
"canvas": "^2.9.3",
"chart.js": "^2.9.4",
"circular-dependency-plugin": "^5.2.2",
"cli-progress": "^3.11.2",
@ -382,9 +382,9 @@
"ignore-loader": "^0.1.2",
"include-media": "^1.4.9",
"jest": "^28.1.2",
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^28.1.1",
"jest-fetch-mock": "^3.0.3",
"jest-image-snapshot": "^5.1.0",
"jest-mock-extended": "^2.0.6",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^2.6.1",
@ -404,6 +404,7 @@
"react-select-event": "^5.5.0",
"react-table": "^7.8.0",
"react-window": "^1.8.7",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.53.0",
"sass-loader": "^12.6.0",
"sharp": "^0.30.7",

View File

@ -0,0 +1,17 @@
/**
* 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 type { Stats } from "fs";
import fsInjectable from "./fs.injectable";
export type Stat = (path: string) => Promise<Stats>;
const statInjectable = getInjectable({
id: "stat",
instantiate: (di): Stat => di.inject(fsInjectable).stat,
});
export default statInjectable;

View File

@ -0,0 +1,18 @@
/**
* 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 { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
const resolvedShellInjectable = getInjectable({
id: "resolved-shell",
instantiate: (di) => {
const store = di.inject(userStoreInjectable);
return computed(() => store.shell || process.env.SHELL || process.env.PTYSHELL);
},
});
export default resolvedShellInjectable;

View File

@ -102,10 +102,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}
@computed get resolvedShell(): string | undefined {
return this.shell || process.env.SHELL || process.env.PTYSHELL;
}
startMainReactions() {
// open at system start-up
reaction(() => this.openAtLogin, openAtLogin => {

View File

@ -4,22 +4,26 @@
*/
export type Disposer = () => void;
interface Extendable<T> {
push(...vals: T[]): void;
export interface LibraryDisposers {
dispose(): void;
}
export type ExtendableDisposer = Disposer & Extendable<Disposer>;
export interface ExtendableDisposer {
(): void;
push(...disposers: (Disposer | LibraryDisposers)[]): void;
}
export function disposer(...args: (Disposer | undefined | null)[]): ExtendableDisposer {
const res = () => {
args.forEach(dispose => dispose?.());
export function disposer(...args: (Disposer | LibraryDisposers | undefined | null)[]): ExtendableDisposer {
return Object.assign(() => {
args.forEach(d => {
if (typeof d === "function") {
d();
} else if (d) {
d.dispose();
}
});
args.length = 0;
};
res.push = (...vals: Disposer[]) => {
args.push(...vals);
};
return res;
}, {
push: (...vals) => args.push(...vals),
} as Pick<ExtendableDisposer, "push">);
}

View File

@ -8,6 +8,7 @@ import configurePackages from "./common/configure-packages";
import { configure } from "mobx";
import { setImmediate } from "timers";
import { TextEncoder, TextDecoder as TextDecoderNode } from "util";
import ResizeObserver from "resize-observer-polyfill";
// setup default configuration for external npm-packages
configurePackages();
@ -36,3 +37,18 @@ process.on("unhandledRejection", (err: any) => {
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoderNode as unknown as typeof TextDecoder;
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
global.ResizeObserver = ResizeObserver;

View File

@ -101,6 +101,9 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import electronInjectable from "./utils/resolve-system-proxy/electron.injectable";
import type { HotbarStore } from "../common/hotbars/store";
import focusApplicationInjectable from "./electron-app/features/focus-application.injectable";
import getValidCwdInjectable from "./shell-session/get-valid-cwd.injectable";
import getCachedShellEnvInjectable from "./shell-session/get-cached-shell-env.injectable";
import spawnPtyInjectable from "./shell-session/spawn-pty.injectable";
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const {
@ -158,6 +161,16 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.override(applicationMenuInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false }));
di.override(getValidCwdInjectable, () => () => Promise.resolve("/some/valid/cwd"));
di.override(getCachedShellEnvInjectable, (di) => ({ cluster }) => Promise.resolve({
NO_PROXY: "localhost,127.0.0.1",
TERM_PROGRAM: di.inject(appNameInjectable),
TERM_PROGRAM_VERSION: di.inject(appVersionInjectable),
KUBECONFIG: `/some/proxy/kubeconfig/${cluster.id}`,
PTYPID: "12345",
PTYSHELL: "zsh",
PATH: process.env.PATH,
}));
overrideFunctionalInjectables(di, [
getHelmChartInjectable,
@ -175,6 +188,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
readJsonFileInjectable,
readFileInjectable,
execFileInjectable,
spawnPtyInjectable,
]);
// TODO: Remove usages of globally exported appEventBus to get rid of this

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 { getInjectable } from "@ogre-tools/injectable";
const cachedShellEnvInjectable = getInjectable({
id: "cached-shell-env",
instantiate: () => new Map<string, Record<string, string | undefined>>(),
});
export default cachedShellEnvInjectable;

View File

@ -0,0 +1,61 @@
/**
* 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 type { IPty } from "node-pty";
import loggerInjectable from "../../common/logger.injectable";
import { getOrInsertWith } from "../../renderer/utils";
import shellProcessesInjectable from "./shell-processes.injectable";
import spawnPtyInjectable from "./spawn-pty.injectable";
export interface EnsuredShellProcess {
shellProcess: IPty;
resume: boolean;
cleanup: () => void;
}
export interface EnsureShellProcessArgs {
shell: string;
args: string[];
env: Record<string, string | undefined>;
cwd: string;
terminalId: string;
}
export type EnsureShellProcess = (args: EnsureShellProcessArgs) => EnsuredShellProcess;
const ensureShellProcessInjectable = getInjectable({
id: "ensure-shell-process",
instantiate: (di): EnsureShellProcess => {
const shellProcesses = di.inject(shellProcessesInjectable);
const logger = di.inject(loggerInjectable);
const spawnPty = di.inject(spawnPtyInjectable);
return ({ shell, args, env, cwd, terminalId } ) => {
const resume = shellProcesses.has(terminalId);
const shellProcess = getOrInsertWith(shellProcesses, terminalId, () => (
spawnPty(shell, args, {
rows: 30,
cols: 80,
cwd,
env: env as Record<string, string>,
name: "xterm-256color",
// TODO: Something else is broken here so we need to force the use of winPty on windows
useConpty: false,
})
));
logger.info(`[SHELL-SESSION]: PTY for ${terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
return {
shellProcess,
resume,
cleanup: () => {
shellProcess.kill();
shellProcesses.delete(terminalId);
},
};
};
},
});
export default ensureShellProcessInjectable;

View File

@ -0,0 +1,37 @@
/**
* 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 cachedShellEnvInjectable from "./cached-shell-env.injectable";
import type { GetShellEnv } from "./get-shell-env.injectable";
import getShellEnvInjectable from "./get-shell-env.injectable";
const getCachedShellEnvInjectable = getInjectable({
id: "get-cached-shell-env",
instantiate: (di): GetShellEnv => {
const cachedShellEnv = di.inject(cachedShellEnvInjectable);
const getShellEnv = di.inject(getShellEnvInjectable);
return async (args) => {
const { id: clusterId } = args.cluster;
let env = cachedShellEnv.get(clusterId);
if (!env) {
env = await getShellEnv(args);
cachedShellEnv.set(clusterId, env);
} else {
// refresh env in the background
getShellEnv(args).then((shellEnv) => {
cachedShellEnv.set(clusterId, shellEnv);
});
}
return env;
};
},
});
export default getCachedShellEnvInjectable;

View File

@ -0,0 +1,86 @@
/**
* 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 path from "path";
import type { Cluster } from "../../common/cluster/cluster";
import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable";
import resolvedShellInjectable from "../../common/user-store/resolved-shell.injectable";
import isWindowsInjectable from "../../common/vars/is-windows.injectable";
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import shellEnvInjectable from "../utils/shell-env.injectable";
export interface GetShellEnvArgs {
cluster: Cluster;
initialPathEntries: string[];
kubectlBinDirP: Promise<string>;
kubeconfigPathP: Promise<string>;
}
export type GetShellEnv = (args: GetShellEnvArgs) => Promise<Record<string, string | undefined>>;
const getShellEnvInjectable = getInjectable({
id: "get-shell-env",
instantiate: (di): GetShellEnv => {
const isWindows = di.inject(isWindowsInjectable);
const shellEnv = di.inject(shellEnvInjectable);
const resolvedShell = di.inject(resolvedShellInjectable);
const appName = di.inject(appNameInjectable);
const appVersion = di.inject(appVersionInjectable);
return async ({ cluster, initialPathEntries, kubeconfigPathP, kubectlBinDirP }) => {
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv())));
const shell = resolvedShell.get();
const initialPATH = [await kubectlBinDirP, ...initialPathEntries, process.env.PATH].join(path.delimiter);
delete env.DEBUG; // don't pass DEBUG into shells
if (isWindows) {
env.SystemRoot = process.env.SystemRoot;
env.PTYSHELL = shell || "powershell.exe";
env.PATH = initialPATH;
env.LENS_SESSION = "true";
env.WSLENV = [
process.env.WSLENV,
"KUBECONFIG/up:LENS_SESSION/u",
]
.filter(Boolean)
.join(":");
} else if (shell !== undefined) {
env.PTYSHELL = shell;
env.PATH = initialPATH;
} else {
env.PTYSHELL = ""; // blank runs the system default shell
}
if (path.basename(env.PTYSHELL) === "zsh") {
env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME;
env.ZDOTDIR = await kubectlBinDirP;
env.DISABLE_AUTO_UPDATE = "true";
}
env.PTYPID = process.pid.toString();
env.KUBECONFIG = await kubeconfigPathP;
env.TERM_PROGRAM = appName;
env.TERM_PROGRAM_VERSION = appVersion;
if (cluster.preferences.httpsProxy) {
env.HTTPS_PROXY = cluster.preferences.httpsProxy;
}
env.NO_PROXY = [
"localhost",
"127.0.0.1",
env.NO_PROXY,
]
.filter(Boolean)
.join();
return env;
};
},
});
export default getShellEnvInjectable;

View File

@ -0,0 +1,63 @@
/**
* 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 { homedir } from "os";
import statInjectable from "../../common/fs/stat.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable";
import isWindowsInjectable from "../../common/vars/is-windows.injectable";
export type GetValidCwd = (cwd: string | undefined, env: Record<string, string | undefined>) => Promise<string>;
const getValidCwdInjectable = getInjectable({
id: "get-valid-cwd",
instantiate: (di): GetValidCwd => {
const stat = di.inject(statInjectable);
const isWindows = di.inject(isWindowsInjectable);
const isMac = di.inject(isMacInjectable);
return async (cwd, env) => {
const cwdOptions = [cwd];
if (isWindows) {
cwdOptions.push(
env.USERPROFILE,
homedir(),
"C:\\",
);
} else {
cwdOptions.push(
env.HOME,
homedir(),
);
if (isMac) {
cwdOptions.push("/Users");
} else {
cwdOptions.push("/home");
}
}
for (const potentialCwd of cwdOptions) {
if (!potentialCwd) {
continue;
}
try {
const stats = await stat(potentialCwd);
if (stats.isDirectory()) {
return potentialCwd;
}
} catch {
// ignore error
}
}
return "."; // Always valid
};
},
});
export default getValidCwdInjectable;

View File

@ -0,0 +1,27 @@
/**
* 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 shellProcessesInjectable from "./shell-processes.injectable";
const killAllShellProcessesInjectable = getInjectable({
id: "kill-all-shell-processes",
instantiate: (di) => {
const shellProcesses = di.inject(shellProcessesInjectable);
return () => {
for (const shellProcess of shellProcesses.values()) {
try {
process.kill(shellProcess.pid);
} catch {
// ignore error
}
}
shellProcesses.clear();
};
},
});
export default killAllShellProcessesInjectable;

View File

@ -6,10 +6,10 @@
import path from "path";
import { UserStore } from "../../../common/user-store";
import type { TerminalShellEnvModify } from "../shell-env-modifier/terminal-shell-env-modify.injectable";
import type { ShellSessionArgs } from "../shell-session";
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import { ShellSession } from "../shell-session";
export interface LocalShellSessionDependencies {
export interface LocalShellSessionDependencies extends ShellSessionDependencies {
terminalShellEnvModify: TerminalShellEnvModify;
readonly baseBundeledBinariesDirectory: string;
}
@ -18,7 +18,7 @@ export class LocalShellSession extends ShellSession {
ShellType = "shell";
constructor(protected readonly dependencies: LocalShellSessionDependencies, args: ShellSessionArgs) {
super(args);
super(dependencies, args);
}
protected getPathEntries(): string[] {

View File

@ -6,18 +6,19 @@
import { v4 as uuid } from "uuid";
import { Watch, CoreV1Api } from "@kubernetes/client-node";
import type { KubeConfig } from "@kubernetes/client-node";
import type { ShellSessionArgs } from "../shell-session";
import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session";
import { ShellOpenError, ShellSession } from "../shell-session";
import { get, once } from "lodash";
import { Node, NodeApi } from "../../../common/k8s-api/endpoints";
import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api";
import logger from "../../logger";
import { TerminalChannels } from "../../../common/terminal/channels";
export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string;
}
export interface NodeShellSessionDependencies extends ShellSessionDependencies {}
export class NodeShellSession extends ShellSession {
ShellType = "node-shell";
@ -26,8 +27,8 @@ export class NodeShellSession extends ShellSession {
protected readonly cwd: string | undefined = undefined;
protected readonly nodeName: string;
constructor({ nodeName, ...args }: NodeShellSessionArgs) {
super(args);
constructor(protected readonly dependencies: NodeShellSessionDependencies, { nodeName, ...args }: NodeShellSessionArgs) {
super(dependencies, args);
this.nodeName = nodeName;
}
@ -39,7 +40,7 @@ export class NodeShellSession extends ShellSession {
const cleanup = once(() => {
coreApi
.deleteNamespacedPod(this.podName, "kube-system")
.catch(error => logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
.catch(error => this.dependencies.logger.warn(`[NODE-SHELL]: failed to remove pod shell`, error));
});
this.websocket.once("close", cleanup);
@ -79,7 +80,7 @@ export class NodeShellSession extends ShellSession {
switch (nodeOs) {
default:
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
this.dependencies.logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
// fallthrough
case "linux":
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
@ -131,7 +132,7 @@ export class NodeShellSession extends ShellSession {
}
protected waitForRunningPod(kc: KubeConfig): Promise<void> {
logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
this.dependencies.logger.debug(`[NODE-SHELL]: waiting for ${this.podName} to be running`);
return new Promise((resolve, reject) => {
new Watch(kc)
@ -150,19 +151,19 @@ export class NodeShellSession extends ShellSession {
},
// done callback is called if the watch terminates normally
(err) => {
logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
this.dependencies.logger.error(`[NODE-SHELL]: ${this.podName} was not created in time`);
reject(err);
},
)
.then(req => {
setTimeout(() => {
logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
this.dependencies.logger.error(`[NODE-SHELL]: aborting wait for ${this.podName}, timing out`);
req.abort();
reject("Pod creation timed out");
}, 2 * 60 * 1000); // 2 * 60 * 1000
})
.catch(error => {
logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
this.dependencies.logger.error(`[NODE-SHELL]: waiting for ${this.podName} failed: ${error}`);
reject(error);
});
});

View File

@ -0,0 +1,13 @@
/**
* 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 type { IPty } from "node-pty";
const shellProcessesInjectable = getInjectable({
id: "shell-processes",
instantiate: () => new Map<string, IPty>(),
});
export default shellProcessesInjectable;

View File

@ -6,19 +6,12 @@
import type { Cluster } from "../../common/cluster/cluster";
import type { Kubectl } from "../kubectl/kubectl";
import type WebSocket from "ws";
import { shellEnv } from "../utils/shell-env";
import { app } from "electron";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import path from "path";
import os from "os";
import { isMac, isWindows } from "../../common/vars";
import { UserStore } from "../../common/user-store";
import * as pty from "node-pty";
import { appEventBus } from "../../common/app-event-bus/event-bus";
import logger from "../logger";
import { stat } from "fs/promises";
import { getOrInsertWith } from "../../common/utils";
import { type TerminalMessage, TerminalChannels } from "../../common/terminal/channels";
import type { EnsureShellProcess } from "./ensure-shell-process.injectable";
import type { GetValidCwd } from "./get-valid-cwd.injectable";
import type { GetShellEnv } from "./get-shell-env.injectable";
import type { Logger } from "../../common/logger";
export class ShellOpenError extends Error {
constructor(message: string, options?: ErrorOptions) {
@ -111,27 +104,16 @@ export interface ShellSessionArgs {
tabId: string;
}
export interface ShellSessionDependencies {
ensureShellProcess: EnsureShellProcess;
getValidCwd: GetValidCwd;
getCachedShellEnv: GetShellEnv;
readonly logger: Logger;
}
export abstract class ShellSession {
abstract readonly ShellType: string;
private static readonly shellEnvs = new Map<string, Record<string, string | undefined>>();
private static readonly processes = new Map<string, pty.IPty>();
/**
* Kill all remaining shell backing processes. Should be called when about to
* quit
*/
public static cleanup(): void {
for (const shellProcess of this.processes.values()) {
try {
process.kill(shellProcess.pid);
} catch {
// ignore error
}
}
this.processes.clear();
}
protected abstract readonly cwd: string | undefined;
protected running = false;
protected readonly kubectlBinDirP: Promise<string>;
@ -141,28 +123,7 @@ export abstract class ShellSession {
protected readonly websocket: WebSocket;
protected readonly cluster: Cluster;
protected abstract readonly cwd: string | undefined;
protected ensureShellProcess(shell: string, args: string[], env: Record<string, string | undefined>, cwd: string): { shellProcess: pty.IPty; resume: boolean } {
const resume = ShellSession.processes.has(this.terminalId);
const shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => (
pty.spawn(shell, args, {
rows: 30,
cols: 80,
cwd,
env: env as Record<string, string>,
name: "xterm-256color",
// TODO: Something else is broken here so we need to force the use of winPty on windows
useConpty: false,
})
));
logger.info(`[SHELL-SESSION]: PTY for ${this.terminalId} is ${resume ? "resumed" : "started"} with PID=${shellProcess.pid}`);
return { shellProcess, resume };
}
constructor({ cluster, kubectl, tabId, websocket }: ShellSessionArgs) {
constructor(protected readonly dependencies: ShellSessionDependencies, { cluster, kubectl, tabId, websocket }: ShellSessionArgs) {
this.cluster = cluster;
this.kubectl = kubectl;
this.websocket = websocket;
@ -175,50 +136,15 @@ export abstract class ShellSession {
this.websocket.send(JSON.stringify(message));
}
protected async getCwd(env: Record<string, string | undefined>): Promise<string> {
const cwdOptions = [this.cwd];
if (isWindows) {
cwdOptions.push(
env.USERPROFILE,
os.homedir(),
"C:\\",
);
} else {
cwdOptions.push(
env.HOME,
os.homedir(),
);
if (isMac) {
cwdOptions.push("/Users");
} else {
cwdOptions.push("/home");
}
}
for (const potentialCwd of cwdOptions) {
if (!potentialCwd) {
continue;
}
try {
const stats = await stat(potentialCwd);
if (stats.isDirectory()) {
return potentialCwd;
}
} catch {
// ignore error
}
}
return "."; // Always valid
}
protected async openShellProcess(shell: string, args: string[], env: Record<string, string | undefined>) {
const cwd = await this.getCwd(env);
const { shellProcess, resume } = this.ensureShellProcess(shell, args, env, cwd);
const cwd = await this.dependencies.getValidCwd(this.cwd, env);
const { shellProcess, resume, cleanup } = this.dependencies.ensureShellProcess({
shell,
args,
env,
cwd,
terminalId: this.terminalId,
});
if (resume) {
this.send({ type: TerminalChannels.CONNECTED });
@ -227,7 +153,7 @@ export abstract class ShellSession {
this.running = true;
shellProcess.onData(data => this.send({ type: TerminalChannels.STDOUT, data }));
shellProcess.onExit(({ exitCode }) => {
logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`);
this.dependencies.logger.info(`[SHELL-SESSION]: shell has exited for ${this.terminalId} closed with exitcode=${exitCode}`);
// This might already be false because of the kill() within the websocket.on("close") handler
if (this.running) {
@ -245,11 +171,11 @@ export abstract class ShellSession {
this.websocket
.on("message", (rawData: unknown): void => {
if (!this.running) {
return void logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
return void this.dependencies.logger.debug(`[SHELL-SESSION]: received message from ${this.terminalId}, but shellProcess isn't running`);
}
if (!(rawData instanceof Buffer)) {
return void logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData });
return void this.dependencies.logger.error(`[SHELL-SESSION]: Received message non-buffer message.`, { rawData });
}
const data = rawData.toString();
@ -265,18 +191,18 @@ export abstract class ShellSession {
shellProcess.resize(message.data.width, message.data.height);
break;
case TerminalChannels.PING:
logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
this.dependencies.logger.silly(`[SHELL-SESSION]: ${this.terminalId} ping!`);
break;
default:
logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
this.dependencies.logger.warn(`[SHELL-SESSION]: unknown or unhandleable message type for ${this.terminalId}`, message);
break;
}
} catch (error) {
logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
this.dependencies.logger.error(`[SHELL-SESSION]: failed to handle message for ${this.terminalId}`, error);
}
})
.once("close", code => {
logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
this.dependencies.logger.info(`[SHELL-SESSION]: websocket for ${this.terminalId} closed with code=${WebSocketCloseEvent[code]}(${code})`, { cluster: this.cluster.getMeta() });
const stopShellSession = this.running
&& (
@ -291,11 +217,10 @@ export abstract class ShellSession {
this.running = false;
try {
logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`);
shellProcess.kill();
ShellSession.processes.delete(this.terminalId);
this.dependencies.logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`);
cleanup();
} catch (error) {
logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error);
this.dependencies.logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error);
}
}
});
@ -307,73 +232,13 @@ export abstract class ShellSession {
return [];
}
protected async getCachedShellEnv() {
const { id: clusterId } = this.cluster;
let env = ShellSession.shellEnvs.get(clusterId);
if (!env) {
env = await this.getShellEnv();
ShellSession.shellEnvs.set(clusterId, env);
} else {
// refresh env in the background
this.getShellEnv().then((shellEnv: any) => {
ShellSession.shellEnvs.set(clusterId, shellEnv);
});
}
return env;
}
protected async getShellEnv() {
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv())));
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter);
const shell = UserStore.getInstance().resolvedShell;
delete env.DEBUG; // don't pass DEBUG into shells
if (isWindows) {
env.SystemRoot = process.env.SystemRoot;
env.PTYSHELL = shell || "powershell.exe";
env.PATH = pathStr;
env.LENS_SESSION = "true";
env.WSLENV = [
process.env.WSLENV,
"KUBECONFIG/up:LENS_SESSION/u",
]
.filter(Boolean)
.join(":");
} else if (shell !== undefined) {
env.PTYSHELL = shell;
env.PATH = pathStr;
} else {
env.PTYSHELL = ""; // blank runs the system default shell
}
if (path.basename(env.PTYSHELL) === "zsh") {
env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME;
env.ZDOTDIR = await this.kubectlBinDirP;
env.DISABLE_AUTO_UPDATE = "true";
}
env.PTYPID = process.pid.toString();
env.KUBECONFIG = await this.kubeconfigPathP;
env.TERM_PROGRAM = app.getName();
env.TERM_PROGRAM_VERSION = app.getVersion();
if (this.cluster.preferences.httpsProxy) {
env.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
}
env.NO_PROXY = [
"localhost",
"127.0.0.1",
env.NO_PROXY,
]
.filter(Boolean)
.join();
return env;
protected getCachedShellEnv() {
return this.dependencies.getCachedShellEnv({
cluster: this.cluster,
initialPathEntries: this.getPathEntries(),
kubeconfigPathP: this.kubeconfigPathP,
kubectlBinDirP: this.kubectlBinDirP,
});
}
protected exit(code = WebSocketCloseEvent.NormalClosure) {

View File

@ -0,0 +1,14 @@
/**
* 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 { spawn } from "node-pty";
const spawnPtyInjectable = getInjectable({
id: "spawn-pty",
instantiate: () => spawn,
causesSideEffects: true,
});
export default spawnPtyInjectable;

View File

@ -4,15 +4,13 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { beforeQuitOfBackEndInjectionToken } from "../runnable-tokens/before-quit-of-back-end-injection-token";
import { ShellSession } from "../../shell-session/shell-session";
import killAllShellProcessesInjectable from "../../shell-session/kill-all-processes.injectable";
const cleanUpShellSessionsInjectable = getInjectable({
id: "clean-up-shell-sessions",
instantiate: () => ({
run: () => {
ShellSession.cleanup();
},
instantiate: (di) => ({
run: di.inject(killAllShellProcessesInjectable),
}),
injectionToken: beforeQuitOfBackEndInjectionToken,

View File

@ -13,7 +13,7 @@ const anyKubeconfig = /^kubeconfig$/i;
* before KUBECONFIG and we only set KUBECONFIG.
* @param env The current copy of env
*/
export function clearKubeconfigEnvVars(env: Record<string, any>): Record<string, any> {
export function clearKubeconfigEnvVars(env: Record<string, string | undefined>): Record<string, string | undefined> {
return Object.fromEntries(
Object.entries(env)
.filter(([key]) => anyKubeconfig.exec(key) === null),

View File

@ -0,0 +1,14 @@
/**
* 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 { shellEnv } from "./shell-env";
const shellEnvInjectable = getInjectable({
id: "shell-env",
instantiate: () => shellEnv,
causesSideEffects: true,
});
export default shellEnvInjectable;

View File

@ -30,7 +30,7 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
return Array.from(elem.current?.querySelectorAll(".Tabs .Tab") ?? []);
};
const renderTab = (tab?: DockTabModel) => {
const renderTab = (tab: DockTabModel | undefined, index: number) => {
if (!tab) {
return null;
}
@ -38,14 +38,37 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
switch (tab.kind) {
case TabKind.CREATE_RESOURCE:
case TabKind.EDIT_RESOURCE:
return <DockTab value={tab} icon="edit" />;
return (
<DockTab
value={tab}
icon="edit"
data-testid={`dock-tab-${index}`}
/>
);
case TabKind.INSTALL_CHART:
case TabKind.UPGRADE_CHART:
return <DockTab value={tab} icon="install_desktop" />;
return (
<DockTab
value={tab}
icon="install_desktop"
data-testid={`dock-tab-${index}`}
/>
);
case TabKind.POD_LOGS:
return <DockTab value={tab} icon="subject" />;
return (
<DockTab
value={tab}
icon="subject"
data-testid={`dock-tab-${index}`}
/>
);
case TabKind.TERMINAL:
return <TerminalTab value={tab} />;
return (
<TerminalTab
value={tab}
data-testid={`dock-tab-${index}`}
/>
);
}
};
@ -96,7 +119,11 @@ export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabs
scrollable={showScrollbar}
className={styles.tabs}
>
{tabs.map(tab => <Fragment key={tab.id}>{renderTab(tab)}</Fragment>)}
{tabs.map((tab, index) => (
<Fragment key={tab.id}>
{renderTab(tab, index)}
</Fragment>
))}
</Tabs>
</div>
);

View File

@ -0,0 +1,14 @@
/**
* 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";
// This is here so that in tests we can override it to disable a warning because we are using jest-canvas-mock
const allowTerminalTransparencyInjectable = getInjectable({
id: "allow-terminal-transparency",
instantiate: () => false,
});
export default allowTerminalTransparencyInjectable;

View File

@ -11,6 +11,7 @@ import terminalSpawningPoolInjectable from "./terminal-spawning-pool.injectable"
import terminalConfigInjectable from "../../../../common/user-store/terminal-config.injectable";
import terminalCopyOnSelectInjectable from "../../../../common/user-store/terminal-copy-on-select.injectable";
import themeStoreInjectable from "../../../themes/store.injectable";
import allowTerminalTransparencyInjectable from "./allow-transparency.injectable";
export type CreateTerminal = (tabId: TabId, api: TerminalApi) => Terminal;
@ -22,6 +23,7 @@ const createTerminalInjectable = getInjectable({
terminalConfig: di.inject(terminalConfigInjectable),
terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable),
themeStore: di.inject(themeStoreInjectable),
allowTransparency: di.inject(allowTerminalTransparencyInjectable),
};
return (tabId, api) => new Terminal(dependencies, { tabId, api });

View File

@ -25,6 +25,7 @@ export interface TerminalDependencies {
readonly terminalConfig: IComputedValue<TerminalConfig>;
readonly terminalCopyOnSelect: IComputedValue<boolean>;
readonly themeStore: ThemeStore;
readonly allowTransparency: boolean;
}
export interface TerminalArguments {
@ -88,6 +89,7 @@ export class Terminal {
cursorStyle: "bar",
fontSize: this.fontSize,
fontFamily: this.fontFamily,
allowTransparency: dependencies.allowTransparency,
});
// enable terminal addons
this.xterm.loadAddon(this.fitAddon);
@ -119,6 +121,9 @@ export class Terminal {
() => this.api.removeAllListeners(),
() => window.removeEventListener("resize", this.onResize),
() => this.elem.removeEventListener("contextmenu", this.onContextMenu),
this.xterm.onResize(({ cols, rows }) => {
this.api.sendTerminalSize(cols, rows);
}),
);
}
@ -127,21 +132,7 @@ export class Terminal {
this.xterm.dispose();
}
fit = () => {
try {
const { cols, rows } = this.fitAddon.proposeDimensions();
// attempt to resize/fit terminal when it's not visible in DOM will crash with exception
// see: https://github.com/xtermjs/xterm.js/issues/3118
if (isNaN(cols) || isNaN(rows)) return;
this.fitAddon.fit();
this.api.sendTerminalSize(cols, rows);
} catch (error) {
// see https://github.com/lensapp/lens/issues/1891
logger.error(`[TERMINAL]: failed to resize terminal to fit`, error);
}
};
fit = () => this.fitAddon.fit();
fitLazy = debounce(this.fit, 250);

View File

@ -66,16 +66,16 @@ type EnableExtensions<T> = (...extensions: T[]) => void;
type DisableExtensions<T> = (...extensions: T[]) => void;
export interface ApplicationBuilder {
dis: DiContainers;
readonly dis: DiContainers;
setEnvironmentToClusterFrame: () => ApplicationBuilder;
extensions: {
renderer: {
readonly extensions: {
readonly renderer: {
enable: EnableExtensions<LensRendererExtension>;
disable: DisableExtensions<LensRendererExtension>;
};
main: {
readonly main: {
enable: EnableExtensions<LensMainExtension>;
disable: DisableExtensions<LensMainExtension>;
};
@ -89,38 +89,42 @@ export interface ApplicationBuilder {
beforeRender: (callback: Callback) => ApplicationBuilder;
render: () => Promise<RenderResult>;
tray: {
readonly tray: {
click: (id: string) => Promise<void>;
get: (id: string) => MinimalTrayMenuItem | null;
getIconPath: () => string;
};
applicationMenu: {
readonly dock: {
click: (index: number) => void;
};
readonly applicationMenu: {
click: (path: string) => void;
};
preferences: {
readonly preferences: {
close: () => void;
navigate: () => void;
navigateTo: (route: Route<any>, params: Partial<NavigateToRouteOptions<any>>) => void;
navigation: {
readonly navigation: {
click: (id: string) => void;
};
};
helmCharts: {
readonly helmCharts: {
navigate: () => void;
};
select: {
readonly select: {
openMenu: (id: string) => void;
selectOption: (menuId: string, labelText: string) => void;
};
}
interface DiContainers {
rendererDi: DiContainer;
mainDi: DiContainer;
readonly rendererDi: DiContainer;
readonly mainDi: DiContainer;
}
interface Environment {
@ -254,6 +258,11 @@ export const getApplicationBuilder = () => {
const enableMainExtension = enableExtensionsFor(mainExtensionsState, mainDi);
const disableRendererExtension = disableExtensionsFor(rendererExtensionsState, rendererDi);
const disableMainExtension = disableExtensionsFor(mainExtensionsState, mainDi);
const dock: ApplicationBuilder["dock"] = {
click: (index) => {
rendered.getByTestId(`dock-tab-${index}`).click();
},
};
const builder: ApplicationBuilder = {
dis,
@ -293,6 +302,14 @@ export const getApplicationBuilder = () => {
},
},
get dock() {
if (environment === environments.clusterFrame) {
return dock;
}
throw new Error("cannot use dock in application frame");
},
tray: {
get: (id: string) => {
const lastCall = last(traySetMenuItemsMock.mock.calls);

View File

@ -69,6 +69,7 @@ import kubeObjectDetailsClusterFrameChildComponentInjectable from "./components/
import kubeconfigDialogClusterFrameChildComponentInjectable from "./components/kubeconfig-dialog/kubeconfig-dialog-cluster-frame-child-component.injectable";
import portForwardDialogClusterFrameChildComponentInjectable from "./port-forward/port-forward-dialog-cluster-frame-child-component.injectable";
import setupSystemCaInjectable from "./frames/root-frame/setup-system-ca.injectable";
import allowTerminalTransparencyInjectable from "./components/dock/terminal/allow-transparency.injectable";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@ -185,7 +186,7 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
isTableColumnHidden: () => false,
extensionRegistryUrl: { customUrl: "some-custom-url" },
syncKubeconfigEntries: observable.map(),
terminalConfig: { fontSize: 42 },
terminalConfig: { fontSize: 42, fontFamily: "RobotoMono" },
editorConfiguration: { minimap: {}, tabSize: 42, fontSize: 42 },
} as unknown as UserStore),
);
@ -197,7 +198,7 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
overrideFsWithFakes(di);
di.override(focusWindowInjectable, () => () => {});
di.override(allowTerminalTransparencyInjectable, () => true);
di.override(loggerInjectable, () => ({
warn: noop,
debug: noop,

View File

@ -0,0 +1,582 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`local shell session techincal tests renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div
class="SidebarCluster"
>
<div
class="Avatar rounded loadingAvatar"
style="width: 40px; height: 40px;"
>
??
</div>
<div
class="loadingClusterName"
/>
</div>
<div
class="sidebarNav sidebar-active-status"
>
<div
class="SidebarItem"
data-is-active-test="true"
data-testid="sidebar-item-workloads"
>
<a
aria-current="page"
class="nav-item flex gaps align-center expandable active"
data-testid="sidebar-item-link-for-workloads"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span
class="link-text box grow"
>
Workloads
</span>
<i
class="Icon expand-icon box right material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-config"
>
<a
class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-config"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="list"
>
list
</span>
</i>
<span
class="link-text box grow"
>
Config
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-network"
>
<a
class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-network"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="device_hub"
>
device_hub
</span>
</i>
<span
class="link-text box grow"
>
Network
</span>
<i
class="Icon expand-icon box right material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-storage"
>
<a
class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-storage"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="storage"
>
storage
</span>
</i>
<span
class="link-text box grow"
>
Storage
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-helm"
>
<a
class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-helm"
href="/"
>
<i
class="Icon svg focusable"
>
<span
class="icon"
/>
</i>
<span
class="link-text box grow"
>
Helm
</span>
<i
class="Icon expand-icon box right material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-user-management"
>
<a
class="nav-item flex gaps align-center"
data-testid="sidebar-item-link-for-user-management"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="security"
>
security
</span>
</i>
<span
class="link-text box grow"
>
Access Control
</span>
</a>
</div>
<div
class="SidebarItem"
data-is-active-test="false"
data-testid="sidebar-item-custom-resources"
>
<a
class="nav-item flex gaps align-center expandable"
data-testid="sidebar-item-link-for-custom-resources"
href="/"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="extension"
>
extension
</span>
</i>
<span
class="link-text box grow"
>
Custom Resources
</span>
<i
class="Icon expand-icon box right material focusable"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
</i>
</a>
</div>
</div>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="TabLayout"
data-testid="tab-layout"
>
<div
class="Tabs center scrollable"
>
<div
class="Tab flex gaps align-center active"
data-is-active-test="true"
data-testid="tab-link-for-overview"
role="tab"
tabindex="0"
>
<div
class="label"
>
Overview
</div>
</div>
</div>
<main>
<div
class="WorkloadsOverview flex column gaps"
>
<div
class="header flex gaps align-center"
>
<h5
class="box grow"
>
Overview
</h5>
<div>
<div
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-overview-namespace-select-filter-input-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control css-1s2u09g-control"
>
<div
class="Select__value-container Select__value-container--is-multi css-319lph-ValueContainer"
>
<div
class="Select__placeholder css-14el2xx-placeholder"
id="react-select-overview-namespace-select-filter-input-placeholder"
>
All namespaces
</div>
<div
class="Select__input-container css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-describedby="react-select-overview-namespace-select-filter-input-placeholder"
aria-expanded="false"
aria-haspopup="true"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="overview-namespace-select-filter-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="OverviewStatuses"
>
<div
class="workloads"
/>
</div>
</div>
</main>
</div>
</div>
<div
class="footer"
>
<div
class="Dock isOpen"
tabindex="-1"
>
<div
class="ResizingAnchor vertical leading"
/>
<div
class="tabs-container flex align-center"
>
<div
class="dockTabs"
role="tablist"
>
<div
class="Tabs tabs"
>
<div
class="Tab flex gaps align-center DockTab TerminalTab active"
data-testid="dock-tab-0"
id="tab-terminal"
role="tab"
tabindex="0"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="terminal"
>
terminal
</span>
</i>
<div
class="label"
>
<div
class="flex align-center"
>
<span
class="title"
>
Terminal
</span>
<div
class="close"
>
<i
class="Icon material interactive focusable small"
id="tooltip_target_13"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
<div />
</i>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="toolbar flex gaps align-center box grow"
>
<div
class="dock-menu box grow"
>
<i
class="Icon new-dock-tab material interactive focusable"
id="menu_actions_14"
tabindex="0"
>
<span
class="icon"
data-icon-name="add"
>
add
</span>
<div />
</i>
</div>
<i
class="Icon material interactive focusable"
id="tooltip_target_16"
tabindex="0"
>
<span
class="icon"
data-icon-name="fullscreen"
>
fullscreen
</span>
<div />
</i>
<i
class="Icon material interactive focusable"
id="tooltip_target_17"
tabindex="0"
>
<span
class="icon"
data-icon-name="keyboard_arrow_down"
>
keyboard_arrow_down
</span>
<div />
</i>
</div>
</div>
<div
class="tab-content terminal"
style="flex-basis: 300px;"
>
<div
class="TerminalWindow dark"
>
<div
class="terminal xterm focus"
dir="ltr"
tabindex="0"
>
<div
class="xterm-viewport"
style="background-color: rgb(0, 0, 0);"
>
<div
class="xterm-scroll-area"
/>
</div>
<div
class="xterm-screen"
>
<div
class="xterm-helpers"
>
<textarea
aria-label="Terminal input"
aria-multiline="false"
autocapitalize="off"
autocorrect="off"
class="xterm-helper-textarea"
spellcheck="false"
tabindex="0"
/>
<span
aria-hidden="true"
class="xterm-char-measure-element"
style="font-family: RobotoMono; font-size: 42px;"
>
W
</span>
<div
class="composition-view"
/>
</div>
<canvas
class="xterm-text-layer"
style="z-index: 0;"
/>
<canvas
class="xterm-selection-layer"
style="z-index: 1;"
/>
<canvas
class="xterm-link-layer"
style="z-index: 2;"
/>
<canvas
class="xterm-cursor-layer"
style="z-index: 3;"
/>
<div
class="xterm-decoration-container"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import getShellAuthTokenChannelHandlerInjectable from "../../main/lens-proxy/proxy-functions/shell/auth-token-channel-handler.injectable";
import type { GetShellAuthToken } from "../../common/shell-authentication/get-auth-token.injectable";
describe("local shell session techincal tests", () => {
let builder: ApplicationBuilder;
let result: RenderResult;
let authenticationSpy: jest.SpyInstance<Uint8Array | Promise<Uint8Array>, Parameters<GetShellAuthToken>>;
beforeEach(async () => {
builder = getApplicationBuilder()
.beforeRender(() => {
const shellAuthentication = builder.dis.mainDi.inject(getShellAuthTokenChannelHandlerInjectable);
authenticationSpy = jest.spyOn(shellAuthentication, "handler");
});
builder.setEnvironmentToClusterFrame();
result = await builder.render();
builder.dock.click(0);
});
it("renders", () => {
expect(result.baseElement).toMatchSnapshot();
});
it("should call the authentication function", () => {
expect(authenticationSpy).toBeCalled();
});
});

198
yarn.lock
View File

@ -1108,6 +1108,21 @@
lodash "^4.17.15"
tmp-promise "^3.0.2"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc"
integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==
dependencies:
detect-libc "^2.0.0"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.7"
nopt "^5.0.0"
npmlog "^5.0.1"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.11"
"@material-ui/core@^4.12.3":
version "4.12.4"
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73"
@ -2944,6 +2959,11 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@ -3028,7 +3048,7 @@ aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2:
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
"aproba@^1.1.2 || 2", aproba@^2.0.0:
"aproba@^1.0.3 || ^2.0.0", "aproba@^1.1.2 || 2", aproba@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
@ -3038,6 +3058,14 @@ archy@~1.0.0:
resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
are-we-there-yet@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
dependencies:
delegates "^1.0.0"
readable-stream "^3.6.0"
are-we-there-yet@~1.1.2:
version "1.1.7"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
@ -3687,6 +3715,15 @@ caniuse-lite@^1.0.30001332:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz#f9aece4ea8156071613b27791547ba0b33f176cf"
integrity sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==
canvas@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/canvas/-/canvas-2.9.3.tgz#8723c4f970442d4cdcedba5221579f9660a58bdb"
integrity sha512-WOUM7ghii5TV2rbhaZkh1youv/vW1/Canev6Yx6BG2W+1S07w8jKZqKkPnbiPpQEDsnJdN8ouDd7OvQEGXDcUw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
nan "^2.15.0"
simple-get "^3.0.3"
capture-stack-trace@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
@ -3697,6 +3734,17 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==
dependencies:
ansi-styles "^2.2.1"
escape-string-regexp "^1.0.2"
has-ansi "^2.0.0"
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -3980,6 +4028,11 @@ color-string@^1.6.0, color-string@^1.9.0:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color-support@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
color@^3.1.3, color@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
@ -4453,11 +4506,6 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssfontparser@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=
cssom@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
@ -4594,6 +4642,13 @@ decompress-response@^3.3.0:
dependencies:
mimic-response "^1.0.0"
decompress-response@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
dependencies:
mimic-response "^2.0.0"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@ -5414,7 +5469,7 @@ escape-html@~1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
escape-string-regexp@^1.0.5:
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
@ -6220,6 +6275,21 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
gauge@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
dependencies:
aproba "^1.0.3 || ^2.0.0"
color-support "^1.1.2"
console-control-strings "^1.0.0"
has-unicode "^2.0.1"
object-assign "^4.1.1"
signal-exit "^3.0.0"
string-width "^4.2.3"
strip-ansi "^6.0.1"
wide-align "^1.1.2"
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -6292,6 +6362,11 @@ get-package-type@^0.1.0:
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
get-stdin@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
integrity sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@ -6434,6 +6509,11 @@ globby@^11.0.1, globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
glur@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689"
integrity sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==
got@^11.8.5:
version "11.8.5"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046"
@ -6547,6 +6627,13 @@ harmony-reflect@^1.4.6:
resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710"
integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==
dependencies:
ansi-regex "^2.0.0"
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
@ -6581,7 +6668,7 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
has-unicode@^2.0.0, has-unicode@~2.0.1:
has-unicode@^2.0.0, has-unicode@^2.0.1, has-unicode@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
@ -7492,14 +7579,6 @@ jake@^10.8.5:
filelist "^1.0.1"
minimatch "^3.0.4"
jest-canvas-mock@^2.3.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
dependencies:
cssfontparser "^1.2.1"
moo-color "^1.0.2"
jest-changed-files@^28.0.2:
version "28.0.2"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-28.0.2.tgz#7d7810660a5bd043af9e9cfbe4d58adb05e91531"
@ -7665,6 +7744,21 @@ jest-haste-map@^28.1.1:
optionalDependencies:
fsevents "^2.3.2"
jest-image-snapshot@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-5.1.0.tgz#8282abab770d6943b5f78498eca548cfebc4c1d5"
integrity sha512-GpTaiJehsVeMRvszSuMkRK0ODSHxr31zalk3qoHVqHKGztgYlJ+Y6AheWHU6azODuoVpMQH52zY/sxri9d/GaA==
dependencies:
chalk "^1.1.3"
get-stdin "^5.0.1"
glur "^1.1.2"
lodash "^4.17.4"
mkdirp "^0.5.1"
pixelmatch "^5.1.0"
pngjs "^3.4.0"
rimraf "^2.6.2"
ssim.js "^3.1.1"
jest-leak-detector@^28.1.1:
version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-28.1.1.tgz#537f37afd610a4b3f4cab15e06baf60484548efb"
@ -8887,6 +8981,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
mimic-response@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
@ -9073,13 +9172,6 @@ monaco-editor@^0.29.1:
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.29.1.tgz#6ee93d8a5320704d48fd7058204deed72429c020"
integrity sha512-rguaEG/zrPQSaKzQB7IfX/PpNa0qxF1FY8ZXRkN4WIl8qZdTQRSRJCtRto7IMcSgrU6H53RXI+fTcywOBC4aVw==
moo-color@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==
dependencies:
color-name "^1.1.4"
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -9125,6 +9217,11 @@ nan@^2.14.0:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
nan@^2.15.0:
version "2.16.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@ -9198,7 +9295,7 @@ node-fetch-npm@^2.0.2:
json-parse-better-errors "^1.0.0"
safe-buffer "^5.1.1"
node-fetch@2.6.7:
node-fetch@2.6.7, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -9600,6 +9697,16 @@ npmlog@^4.1.2, npmlog@~4.1.2:
gauge "~2.7.3"
set-blocking "~2.0.0"
npmlog@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
dependencies:
are-we-there-yet "^2.0.0"
console-control-strings "^1.1.0"
gauge "^3.0.0"
set-blocking "^2.0.0"
nth-check@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
@ -10151,6 +10258,13 @@ pirates@^4.0.4:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
pixelmatch@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a"
integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==
dependencies:
pngjs "^6.0.0"
pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@ -10185,6 +10299,16 @@ plist@^3.0.1, plist@^3.0.4:
base64-js "^1.5.1"
xmlbuilder "^9.0.7"
pngjs@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
pngjs@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
popper.js@1.16.1-lts:
version "1.16.1-lts"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05"
@ -11086,6 +11210,11 @@ reserved-words@^0.1.2:
resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-alpn@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
@ -11550,6 +11679,15 @@ simple-concat@^1.0.0:
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^3.0.3:
version "3.1.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55"
integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==
dependencies:
decompress-response "^4.2.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-get@^4.0.0, simple-get@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
@ -11796,6 +11934,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
ssim.js@^3.1.1:
version "3.5.0"
resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df"
integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==
ssri@^6.0.0, ssri@^6.0.1, ssri@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
@ -12073,6 +12216,11 @@ sumchecker@^3.0.1:
dependencies:
debug "^4.1.0"
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==
supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@ -13252,7 +13400,7 @@ which@^2.0.1, which@^2.0.2:
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0:
wide-align@^1.1.0, wide-align@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==