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

chore: Fix lint for non @k8slens/core packages

- Update eslint config to be tighter in control

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-04-27 12:13:01 -04:00
parent fcef072859
commit 6184c2f03b
29 changed files with 291 additions and 292 deletions

View File

@ -18,7 +18,7 @@ import platformInjectable from "./platform.injectable";
describe("keyboard-shortcuts", () => { describe("keyboard-shortcuts", () => {
let di: DiContainer; let di: DiContainer;
let invokeMock: jest.Mock; let invokeMock: jest.MockedFunction<(val?: string) => void>;
let rendered: RenderResult; let rendered: RenderResult;
beforeEach(() => { beforeEach(() => {

View File

@ -1,6 +1,7 @@
module.exports = { module.exports = {
extends: [ extends: [
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"react-app", "react-app",
"react-app/jest", "react-app/jest",
"airbnb-typescript", "airbnb-typescript",
@ -15,14 +16,16 @@ module.exports = {
"xss", "xss",
"no-unsanitized" "no-unsanitized"
], ],
ignorePatterns: [ ignorePatterns: ["**/dist/**"],
"dist/*"
],
rules: { rules: {
"react/react-in-jsx-scope": 0, "react/react-in-jsx-scope": 0,
"security/detect-object-injection": "off", "security/detect-object-injection": "off",
"security/detect-non-literal-fs-filename": "off" "security/detect-non-literal-fs-filename": "off"
}, },
parserOptions: {
project: true,
tsconfigRootDir: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
},
overrides: [ overrides: [
{ {
files: [ files: [
@ -121,6 +124,9 @@ module.exports = {
"template-curly-spacing": "error", "template-curly-spacing": "error",
"keyword-spacing": "off", "keyword-spacing": "off",
// jest rules
"jest/valid-title": "off",
// testing-library // testing-library
"testing-library/no-node-access": "off", "testing-library/no-node-access": "off",
"testing-library/no-container": "off", "testing-library/no-container": "off",
@ -132,11 +138,9 @@ module.exports = {
"@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-useless-constructor": "off", "@typescript-eslint/no-useless-constructor": "off",
"@typescript-eslint/comma-dangle": "off", "@typescript-eslint/comma-dangle": "off",
"@typescript-eslint/no-shadow": "off", "@typescript-eslint/no-shadow": "off",
@ -148,15 +152,25 @@ module.exports = {
allowTemplateLiterals: true, allowTemplateLiterals: true,
}, },
], ],
"@typescript-eslint/no-unused-expressions": [ "@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/keyword-spacing": "error",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/restrict-template-expressions": [
"error", "error",
{ {
allowShortCircuit: true, allowNumber: true,
allowBoolean: true,
allowNullish: true,
}, },
], ],
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-use-before-define": [
"@typescript-eslint/keyword-spacing": ["error"], "error",
"@typescript-eslint/naming-convention": "off", {
functions: false,
classes: false,
}
],
// React specific rules // React specific rules
"react-hooks/rules-of-hooks": "error", "react-hooks/rules-of-hooks": "error",

View File

@ -21,11 +21,10 @@ export const observableHistoryInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const history = di.inject(historyInjectable); const history = di.inject(historyInjectable);
const navigation = createObservableHistory(history, {
return createObservableHistory(history, {
searchParams: searchParamsOptions, searchParams: searchParamsOptions,
}); });
return navigation;
}, },
injectionToken: observableHistoryInjectionToken, injectionToken: observableHistoryInjectionToken,
}); });

View File

@ -98,14 +98,15 @@ describe("register-feature", () => {
}); });
it("given di-container and registered Features with injectables forming a cycle, when an injectable is injected, throws with namespaced error about cycle", () => { it("given di-container and registered Features with injectables forming a cycle, when an injectable is injected, throws with namespaced error about cycle", () => {
const someInjectable: Injectable<any> = getInjectable({ const someInjectable: Injectable<unknown> = getInjectable({
id: "some-injectable-1", id: "some-injectable-1",
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
instantiate: (di) => di.inject(someInjectable2), instantiate: (di) => di.inject(someInjectable2),
}); });
const someInjectable2: Injectable<any> = getInjectable({ const someInjectable2: Injectable<unknown> = getInjectable({
id: "some-injectable-2", id: "some-injectable-2",
// eslint-disable-next-line @typescript-eslint/no-use-before-define
instantiate: (di) => di.inject(someInjectable), instantiate: (di) => di.inject(someInjectable),
}); });

View File

@ -1,31 +1,31 @@
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { enlistMessageChannelListenerInjectionToken } from "../message/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "../message/enlist-message-channel-listener-injection-token";
import { getStartableStoppable, StartableStoppable } from "@k8slens/startable-stoppable"; import { getStartableStoppable, StartableStoppable } from "@k8slens/startable-stoppable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import { IComputedValue, reaction } from "mobx"; import { IComputedValue, reaction } from "mobx";
import { import {
MessageChannel, MessageChannel,
MessageChannelListener,
messageChannelListenerInjectionToken, messageChannelListenerInjectionToken,
} from "../message/message-channel-listener-injection-token"; } from "../message/message-channel-listener-injection-token";
import { import {
RequestChannel, RequestChannel,
RequestChannelListener,
requestChannelListenerInjectionToken, requestChannelListenerInjectionToken,
} from "../request/request-channel-listener-injection-token"; } from "../request/request-channel-listener-injection-token";
import { enlistRequestChannelListenerInjectionToken } from "../request/enlist-request-channel-listener-injection-token"; import { enlistRequestChannelListenerInjectionToken } from "../request/enlist-request-channel-listener-injection-token";
import type { Disposer } from "@k8slens/utilities";
export type ListeningOfChannels = StartableStoppable; export type ListeningOfChannels = StartableStoppable;
export const listeningOfChannelsInjectionToken = getInjectionToken<ListeningOfChannels>({ export const listeningOfChannelsInjectionToken = getInjectionToken<ListeningOfChannels>({
id: "listening-of-channels-injection-token", id: "listening-of-channels-injection-token",
}); });
const listening = <T extends { id: string; channel: MessageChannel<any> | RequestChannel<any, any> }>( function messageListening<Message>(
channelListeners: IComputedValue<T[]>, channelListeners: IComputedValue<MessageChannelListener<MessageChannel<Message>>[]>,
enlistChannelListener: (listener: T) => () => void, enlistChannelListener: (listener: MessageChannelListener<MessageChannel<Message>>) => Disposer,
getId: (listener: T) => string, ): Disposer {
) => {
const listenerDisposers = new Map<string, () => void>(); const listenerDisposers = new Map<string, () => void>();
const reactionDisposer = reaction( const reactionDisposer = reaction(
@ -38,23 +38,20 @@ const listening = <T extends { id: string; channel: MessageChannel<any> | Reques
); );
addedListeners.forEach((listener) => { addedListeners.forEach((listener) => {
const id = getId(listener); if (listenerDisposers.has(listener.id)) {
throw new Error(
if (listenerDisposers.has(id)) { `Tried to add listener "${listener.id}" for channel "${listener.channel.id}" but listener with a same ID already exists.`,
throw new Error(`Tried to add listener for channel "${listener.channel.id}" but listener already exists.`); );
} }
const disposer = enlistChannelListener(listener); const disposer = enlistChannelListener(listener);
listenerDisposers.set(id, disposer); listenerDisposers.set(listener.id, disposer);
}); });
removedListeners.forEach((listener) => { removedListeners.forEach((listener) => {
const dispose = listenerDisposers.get(getId(listener)); listenerDisposers.get(listener.id)?.();
listenerDisposers.delete(listener.id);
dispose?.();
listenerDisposers.delete(getId(listener));
}); });
}, },
@ -65,34 +62,69 @@ const listening = <T extends { id: string; channel: MessageChannel<any> | Reques
reactionDisposer(); reactionDisposer();
listenerDisposers.forEach((dispose) => dispose()); listenerDisposers.forEach((dispose) => dispose());
}; };
}; }
function requestListening<Request, Response>(
channelListeners: IComputedValue<RequestChannelListener<RequestChannel<Request, Response>>[]>,
enlistChannelListener: (listener: RequestChannelListener<RequestChannel<Request, Response>>) => Disposer,
): Disposer {
const listenerDisposers = new Map<string, () => void>();
const reactionDisposer = reaction(
() => channelListeners.get(),
(newValues, oldValues = []) => {
const addedListeners = newValues.filter(
(newValue) => !oldValues.some((oldValue) => oldValue.channel.id === newValue.channel.id),
);
const removedListeners = oldValues.filter(
(oldValue) => !newValues.some((newValue) => newValue.channel.id === oldValue.channel.id),
);
addedListeners.forEach((listener) => {
if (listenerDisposers.has(listener.channel.id)) {
throw new Error(
`Tried to add request listener for channel "${listener.channel.id}" but listener already exists.`,
);
}
const disposer = enlistChannelListener(listener);
listenerDisposers.set(listener.channel.id, disposer);
});
removedListeners.forEach((listener) => {
const dispose = listenerDisposers.get(listener.channel.id);
dispose?.();
listenerDisposers.delete(listener.channel.id);
});
},
{ fireImmediately: true },
);
return () => {
reactionDisposer();
listenerDisposers.forEach((dispose) => dispose());
};
}
const listeningOfChannelsInjectable = getInjectable({ const listeningOfChannelsInjectable = getInjectable({
id: "listening-of-channels", id: "listening-of-channels",
instantiate: (di) => { instantiate: (di) => {
const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken); const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectionToken); const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectionToken);
const computedInjectMany = di.inject(computedInjectManyInjectable); const computedInjectMany = di.inject(computedInjectManyInjectable);
const messageChannelListeners = computedInjectMany(messageChannelListenerInjectionToken); const messageChannelListeners = computedInjectMany(messageChannelListenerInjectionToken);
const requestChannelListeners = computedInjectMany(requestChannelListenerInjectionToken); const requestChannelListeners = computedInjectMany(requestChannelListenerInjectionToken);
return getStartableStoppable("listening-of-channels", () => { return getStartableStoppable("listening-of-channels", () => {
const stopListeningOfMessageChannels = listening( const stopListeningOfMessageChannels = messageListening(messageChannelListeners, enlistMessageChannelListener);
messageChannelListeners, const stopListeningOfRequestChannels = requestListening(requestChannelListeners, enlistRequestChannelListener);
enlistMessageChannelListener,
(x) => x.id,
);
const stopListeningOfRequestChannels = listening(
requestChannelListeners,
enlistRequestChannelListener,
(x) => x.channel.id,
);
return () => { return () => {
stopListeningOfMessageChannels(); stopListeningOfMessageChannels();

View File

@ -9,8 +9,8 @@ const startListeningOfChannelsInjectable = getInjectable({
const listeningOfChannels = di.inject(listeningOfChannelsInjectionToken); const listeningOfChannels = di.inject(listeningOfChannelsInjectionToken);
return { return {
run: async () => { run: () => {
await listeningOfChannels.start(); listeningOfChannels.start();
}, },
}; };
}, },

View File

@ -12,7 +12,6 @@ export type RequestChannelHandler<Channel> = Channel extends RequestChannel<infe
: never; : never;
export interface RequestChannelListener<Channel> { export interface RequestChannelListener<Channel> {
id: string;
channel: Channel; channel: Channel;
handler: RequestChannelHandler<Channel>; handler: RequestChannelHandler<Channel>;
} }

View File

@ -17,6 +17,7 @@ import {
ComputedChannelAdminMessage, ComputedChannelAdminMessage,
} from "./computed-channel-administration-channel.injectable"; } from "./computed-channel-administration-channel.injectable";
import { computedChannelFeature } from "../feature"; import { computedChannelFeature } from "../feature";
import type { RenderResult } from "@testing-library/react";
const testChannel: MessageChannel<string> = { id: "some-channel-id" }; const testChannel: MessageChannel<string> = { id: "some-channel-id" };
const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" }; const testChannel2: MessageChannel<string> = { id: "some-other-channel-id" };
@ -110,7 +111,7 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
}); });
describe("when observing the computed value in a component in di-1", () => { describe("when observing the computed value in a component in di-1", () => {
let rendered: any; let rendered: RenderResult;
beforeEach(() => { beforeEach(() => {
const render = renderFor(di2); const render = renderFor(di2);
@ -118,13 +119,10 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
rendered = render(<TestComponent someComputed={computedTestChannel} />); rendered = render(<TestComponent someComputed={computedTestChannel} />);
}); });
const scenarioName = scenarioIsAsync ? "when all messages are propagated" : "immediately"; describe(scenarioIsAsync ? "when all messages are propagated" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagationRecursive(act).then(done); void messageBridgeFake.messagePropagationRecursive(act).then(done);
} else { } else {
done(); done();
} }
@ -155,15 +153,13 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
); );
}); });
scenarioIsAsync && if (scenarioIsAsync) {
it("computed test channel value is observed as the pending value", () => { it("computed test channel value is observed as the pending value", () => {
expect(observedValue).toBe("some-pending-value"); expect(observedValue).toBe("some-pending-value");
}); });
}
const scenarioName = scenarioIsAsync ? "when admin messages are propagated" : "immediately"; describe(scenarioIsAsync ? "when admin messages are propagated" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
void messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
@ -179,10 +175,7 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
}); });
}); });
const scenarioName = scenarioIsAsync ? "when returning value-messages propagate" : "immediately"; describe(scenarioIsAsync ? "when returning value-messages propagate" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
void messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
@ -208,10 +201,7 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
}); });
}); });
const scenarioName = scenarioIsAsync ? "when value-messages propagate" : "immediately"; describe(scenarioIsAsync ? "when value-messages propagate" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
void messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
@ -237,10 +227,7 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
stopObserving(); stopObserving();
}); });
const scenarioName = scenarioIsAsync ? "when admin-messages propagate" : "immediately"; describe(scenarioIsAsync ? "when admin-messages propagate" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
void messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
@ -289,15 +276,13 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
); );
}); });
scenarioIsAsync && if (scenarioIsAsync) {
it("computed test channel value is observed as the pending value again", () => { it("computed test channel value is observed as the pending value again", () => {
expect(observedValue).toBe("some-pending-value"); expect(observedValue).toBe("some-pending-value");
}); });
}
const scenarioName = scenarioIsAsync ? "when admin messages propagate" : "immediately"; describe(scenarioIsAsync ? "when admin messages propagate" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
latestAdminMessage = undefined; latestAdminMessage = undefined;
@ -315,14 +300,14 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
}); });
}); });
scenarioIsAsync && if (scenarioIsAsync) {
it("computed test channel value is still observed as the pending value", () => { it("computed test channel value is still observed as the pending value", () => {
expect(observedValue).toBe("some-pending-value"); expect(observedValue).toBe("some-pending-value");
}); });
}
const scenarioTitle = scenarioIsAsync ? "when value-messages propagate back" : "immediately"; const scenarioTitle = scenarioIsAsync ? "when value-messages propagate back" : "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioTitle, () => { describe(scenarioTitle, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
@ -381,17 +366,23 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
}, },
); );
scenarioIsAsync && (await messageBridgeFake.messagePropagation()); if (scenarioIsAsync) {
await messageBridgeFake.messagePropagation();
}
stopObserving(); stopObserving();
scenarioIsAsync && (await messageBridgeFake.messagePropagation()); if (scenarioIsAsync) {
await messageBridgeFake.messagePropagation();
}
runInAction(() => { runInAction(() => {
someOtherObservable.set("some-value"); someOtherObservable.set("some-value");
}); });
scenarioIsAsync && (await messageBridgeFake.messagePropagation()); if (scenarioIsAsync) {
await messageBridgeFake.messagePropagation();
}
expect(observedValue).toBe("some-value"); expect(observedValue).toBe("some-value");
}); });
@ -439,10 +430,7 @@ const TestComponent = observer(({ someComputed }: { someComputed: IComputedValue
expect(nonReactiveValue).toBe("some-initial-value"); expect(nonReactiveValue).toBe("some-initial-value");
}); });
const scenarioName = scenarioIsAsync ? "when messages would be propagated" : "immediately"; describe(scenarioIsAsync ? "when messages would be propagated" : "immediately", () => {
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
void messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);

View File

@ -1,42 +1,38 @@
import { onLoadOfApplicationInjectionToken } from "@k8slens/application"; import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
import { pipeline } from "@ogre-tools/fp"; import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import { filter, groupBy, nth, map, toPairs } from "lodash/fp";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { computedChannelObserverInjectionToken } from "./computed-channel.injectable"; import { computedChannelObserverInjectionToken } from "./computed-channel.injectable";
export const duplicateChannelObserverGuardInjectable = getInjectable({ export const duplicateChannelObserverGuardInjectable = getInjectable({
id: "duplicate-channel-observer-guard", id: "duplicate-channel-observer-guard",
instantiate: (di) => { instantiate: (di) => ({
const computedInjectMany = di.inject(computedInjectManyInjectable); run: () => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
const computedObservers = computedInjectMany(computedChannelObserverInjectionToken);
return { reaction(
run: () => { () => computedObservers.get(),
reaction( (observers) => {
() => computedInjectMany(computedChannelObserverInjectionToken).get(), const observersByChannelId = iter
(observers) => { .chain(observers.values())
const duplicateObserverChannelIds = pipeline( .map((observer) => [observer.channel.id, observer] as const)
observers, .groupIntoMap();
groupBy((observer) => observer.channel.id), const duplicateIds = iter
toPairs, .chain(observersByChannelId.entries())
filter(([, channelObservers]) => channelObservers.length > 1), .filter(([, channelObservers]) => channelObservers.length > 1)
map(nth(0)), .map(([id]) => id)
); .toArray();
if (duplicateObserverChannelIds.length) { if (duplicateIds.length) {
throw new Error( throw new Error(`Tried to register duplicate channel observer for channels "${duplicateIds.join('", "')}"`);
`Tried to register duplicate channel observer for channels "${duplicateObserverChannelIds.join( }
'", "', },
)}"`, );
); },
} }),
},
);
},
};
},
injectionToken: onLoadOfApplicationInjectionToken, injectionToken: onLoadOfApplicationInjectionToken,
}); });

View File

@ -1,7 +1,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { IpcMainEvent } from "electron"; import type { IpcMainEvent } from "electron";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable"; import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import { enlistMessageChannelListenerInjectionToken, MessageChannel, MessageChannelListener } from "@k8slens/messaging";
const enlistMessageChannelListenerInjectable = getInjectable({ const enlistMessageChannelListenerInjectable = getInjectable({
id: "enlist-message-channel-listener", id: "enlist-message-channel-listener",
@ -9,8 +9,8 @@ const enlistMessageChannelListenerInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable); const ipcMain = di.inject(ipcMainInjectable);
return ({ channel, handler }) => { return <T>({ channel, handler }: MessageChannelListener<MessageChannel<T>>) => {
const nativeOnCallback = (nativeEvent: IpcMainEvent, message: any) => { const nativeOnCallback = (nativeEvent: IpcMainEvent, message: T) => {
handler(message, { frameId: nativeEvent.frameId, processId: nativeEvent.processId }); handler(message, { frameId: nativeEvent.frameId, processId: nativeEvent.processId });
}; };

View File

@ -57,6 +57,7 @@ describe("enlist message channel listener in main", () => {
describe("when message arrives", () => { describe("when message arrives", () => {
beforeEach(() => { beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, "some-message"); onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, "some-message");
}); });
@ -72,18 +73,21 @@ describe("enlist message channel listener in main", () => {
}); });
it("given number as message, when message arrives, calls the handler with the message", () => { it("given number as message, when message arrives, calls the handler with the message", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, 42); onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, 42);
expect(handlerMock).toHaveBeenCalledWith(42, { frameId: 42, processId: 84 }); expect(handlerMock).toHaveBeenCalledWith(42, { frameId: 42, processId: 84 });
}); });
it("given boolean as message, when message arrives, calls the handler with the message", () => { it("given boolean as message, when message arrives, calls the handler with the message", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, true); onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, true);
expect(handlerMock).toHaveBeenCalledWith(true, { frameId: 42, processId: 84 }); expect(handlerMock).toHaveBeenCalledWith(true, { frameId: 42, processId: 84 });
}); });
it("given object as message, when message arrives, calls the handler with the message", () => { it("given object as message, when message arrives, calls the handler with the message", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, { some: "object" }); onMock.mock.calls[0][1]({ frameId: 42, processId: 84 } as IpcMainEvent, { some: "object" });
expect(handlerMock).toHaveBeenCalledWith({ some: "object" }, { frameId: 42, processId: 84 }); expect(handlerMock).toHaveBeenCalledWith({ some: "object" }, { frameId: 42, processId: 84 });

View File

@ -48,7 +48,6 @@ describe("enlist request channel listener in main", () => {
handlerMock = asyncFn(); handlerMock = asyncFn();
disposer = enlistRequestChannelListener({ disposer = enlistRequestChannelListener({
id: "some-listener",
channel: testRequestChannel, channel: testRequestChannel,
handler: handlerMock, handler: handlerMock,
}); });
@ -67,9 +66,11 @@ describe("enlist request channel listener in main", () => {
}); });
describe("when request arrives", () => { describe("when request arrives", () => {
let actualPromise: Promise<any>; let actualPromise: Promise<unknown>;
beforeEach(() => { beforeEach(() => {
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
actualPromise = handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, "some-request"); actualPromise = handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, "some-request");
}); });
@ -127,18 +128,21 @@ describe("enlist request channel listener in main", () => {
}); });
it("given number as request, when request arrives, calls the handler with the request", () => { it("given number as request, when request arrives, calls the handler with the request", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42); handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42);
expect(handlerMock).toHaveBeenCalledWith(42); expect(handlerMock).toHaveBeenCalledWith(42);
}); });
it("given boolean as request, when request arrives, calls the handler with the request", () => { it("given boolean as request, when request arrives, calls the handler with the request", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true); handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true);
expect(handlerMock).toHaveBeenCalledWith(true); expect(handlerMock).toHaveBeenCalledWith(true);
}); });
it("given object as request, when request arrives, calls the handler with the request", () => { it("given object as request, when request arrives, calls the handler with the request", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, { some: "object" }); handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, { some: "object" });
expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); expect(handlerMock).toHaveBeenCalledWith({ some: "object" });

View File

@ -1,4 +1,3 @@
/* c8 ignore start */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { RequestChannel, RequestFromChannel, requestFromChannelInjectionToken } from "@k8slens/messaging"; import { RequestChannel, RequestFromChannel, requestFromChannelInjectionToken } from "@k8slens/messaging";
@ -6,12 +5,10 @@ const requestFromChannelInjectable = getInjectable({
id: "request-from-channel", id: "request-from-channel",
instantiate: () => instantiate: () =>
((channel: RequestChannel<any, any>) => { (async <Request, Response>(channel: RequestChannel<Request, Response>) => {
throw new Error(`Tried to request from channel "${channel.id}" but requesting in "main" it's not supported yet`); throw new Error(`Tried to request from channel "${channel.id}" from main, which is not supported.`);
}) as unknown as RequestFromChannel, }) as RequestFromChannel,
injectionToken: requestFromChannelInjectionToken, injectionToken: requestFromChannelInjectionToken,
}); });
export default requestFromChannelInjectable; export default requestFromChannelInjectable;
/* c8 ignore stop */

View File

@ -10,8 +10,10 @@ const allowCommunicationListenerInjectable = getMessageChannelListenerInjectable
getHandler: (di) => { getHandler: (di) => {
const frameIds = di.inject(frameIdsInjectable); const frameIds = di.inject(frameIdsInjectable);
return (_, { frameId, processId }) => { return (_, info) => {
frameIds.add({ frameId, processId }); if (info) {
frameIds.add(info);
}
}; };
}, },
}); });

View File

@ -17,7 +17,7 @@ describe("send-message-to-channel", () => {
registerFeature(di, messagingFeatureForMain); registerFeature(di, messagingFeatureForMain);
}); });
it("given no web contents, when sending a message, does not do anything", () => { it("given no web contents, when sending a message, does not do an unknown thing", () => {
di.override(getWebContentsInjectable, () => () => []); di.override(getWebContentsInjectable, () => () => []);
const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken);
@ -26,8 +26,8 @@ describe("send-message-to-channel", () => {
}); });
describe("given web content that is alive", () => { describe("given web content that is alive", () => {
let sendToFrameMock: jest.Mock; let sendToFrameMock: jest.MockedFunction<(...args: unknown[]) => void>;
let sendMessageMock: jest.Mock; let sendMessageMock: jest.MockedFunction<(...args: unknown[]) => void>;
beforeEach(() => { beforeEach(() => {
sendToFrameMock = jest.fn(); sendToFrameMock = jest.fn();
@ -35,14 +35,14 @@ describe("send-message-to-channel", () => {
di.override(getWebContentsInjectable, () => () => [ di.override(getWebContentsInjectable, () => () => [
{ {
send: (...args: any[]) => sendMessageMock("first", ...args), send: (...args: unknown[]) => sendMessageMock("first", ...args),
sendToFrame: (...args: any[]) => sendToFrameMock("first", ...args), sendToFrame: (...args: unknown[]) => sendToFrameMock("first", ...args),
isDestroyed: () => false, isDestroyed: () => false,
isCrashed: () => false, isCrashed: () => false,
} as unknown as WebContents, } as unknown as WebContents,
{ {
send: (...args: any[]) => sendMessageMock("second", ...args), send: (...args: unknown[]) => sendMessageMock("second", ...args),
sendToFrame: (...args: any[]) => sendToFrameMock("second", ...args), sendToFrame: (...args: unknown[]) => sendToFrameMock("second", ...args),
isDestroyed: () => false, isDestroyed: () => false,
isCrashed: () => false, isCrashed: () => false,
} as unknown as WebContents, } as unknown as WebContents,

View File

@ -1,18 +1,15 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { pipeline } from "@ogre-tools/fp";
import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging"; import { SendMessageToChannel, sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import getWebContentsInjectable from "./get-web-contents.injectable"; import getWebContentsInjectable from "./get-web-contents.injectable";
import { flatMap, reject } from "lodash/fp";
import type { WebContents } from "electron"; import type { WebContents } from "electron";
import frameIdsInjectable from "./frameIds.injectable"; import frameIdsInjectable from "./frameIds.injectable";
const isDestroyed = (webContent: WebContents) => webContent.isDestroyed(); const isDestroyed = (webContent: WebContents) => webContent.isDestroyed();
const isCrashed = (webContent: WebContents) => webContent.isCrashed(); const isCrashed = (webContent: WebContents) => webContent.isCrashed();
const not =
const forEach = <T>(fn: (val: T) => boolean) =>
<T>(predicate: (item: T) => void) => (val: T) =>
(items: T[]) => !fn(val);
items.forEach(predicate);
const sendMessageToChannelInjectable = getInjectable({ const sendMessageToChannelInjectable = getInjectable({
id: "send-message-to-channel", id: "send-message-to-channel",
@ -22,23 +19,19 @@ const sendMessageToChannelInjectable = getInjectable({
const frameIds = di.inject(frameIdsInjectable); const frameIds = di.inject(frameIdsInjectable);
return ((channel, message) => { return ((channel, message) => {
pipeline( getWebContents()
getWebContents(), .filter(not(isDestroyed))
reject(isDestroyed), .filter(not(isCrashed))
reject(isCrashed), .flatMap((webContent) => [
(channelId: string, ...args: unknown[]) => webContent.send(channelId, ...args),
flatMap((webContent) => [ ...[...frameIds].map(({ frameId, processId }) => (channelId: string, ...args: unknown[]) => {
(channelId: string, ...args: any[]) => webContent.send(channelId, ...args),
...[...frameIds].map(({ frameId, processId }) => (channelId: string, ...args: any[]) => {
webContent.sendToFrame([processId, frameId], channelId, ...args); webContent.sendToFrame([processId, frameId], channelId, ...args);
}), }),
]), ])
.forEach((send) => {
forEach((send) => {
send(channel.id, message); send(channel.id, message);
}), });
);
}) as SendMessageToChannel; }) as SendMessageToChannel;
}, },

View File

@ -1,7 +1,7 @@
import ipcRendererInjectable from "../ipc/ipc-renderer.injectable"; import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { IpcRendererEvent } from "electron"; import type { IpcRendererEvent } from "electron";
import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; import { enlistMessageChannelListenerInjectionToken, MessageChannelListener, MessageChannel } from "@k8slens/messaging";
const enlistMessageChannelListenerInjectable = getInjectable({ const enlistMessageChannelListenerInjectable = getInjectable({
id: "enlist-message-channel-listener-for-renderer", id: "enlist-message-channel-listener-for-renderer",
@ -9,8 +9,8 @@ const enlistMessageChannelListenerInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable); const ipcRenderer = di.inject(ipcRendererInjectable);
return ({ channel, handler }) => { return <T>({ channel, handler }: MessageChannelListener<MessageChannel<T>>) => {
const nativeCallback = (_: IpcRendererEvent, message: any) => { const nativeCallback = (_: IpcRendererEvent, message: T) => {
handler(message); handler(message);
}; };

View File

@ -57,6 +57,7 @@ describe("enlist message channel listener in renderer", () => {
describe("when message arrives", () => { describe("when message arrives", () => {
beforeEach(() => { beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({} as IpcRendererEvent, "some-message"); onMock.mock.calls[0][1]({} as IpcRendererEvent, "some-message");
}); });
@ -72,18 +73,21 @@ describe("enlist message channel listener in renderer", () => {
}); });
it("given number as message, when message arrives, calls the handler with the message", () => { it("given number as message, when message arrives, calls the handler with the message", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({} as IpcRendererEvent, 42); onMock.mock.calls[0][1]({} as IpcRendererEvent, 42);
expect(handlerMock).toHaveBeenCalledWith(42); expect(handlerMock).toHaveBeenCalledWith(42);
}); });
it("given boolean as message, when message arrives, calls the handler with the message", () => { it("given boolean as message, when message arrives, calls the handler with the message", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({} as IpcRendererEvent, true); onMock.mock.calls[0][1]({} as IpcRendererEvent, true);
expect(handlerMock).toHaveBeenCalledWith(true); expect(handlerMock).toHaveBeenCalledWith(true);
}); });
it("given object as message, when message arrives, calls the handler with the message", () => { it("given object as message, when message arrives, calls the handler with the message", () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
onMock.mock.calls[0][1]({} as IpcRendererEvent, { some: "object" }); onMock.mock.calls[0][1]({} as IpcRendererEvent, { some: "object" });
expect(handlerMock).toHaveBeenCalledWith({ some: "object" }); expect(handlerMock).toHaveBeenCalledWith({ some: "object" });

View File

@ -4,7 +4,11 @@ import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
const invokeIpcInjectable = getInjectable({ const invokeIpcInjectable = getInjectable({
id: "invoke-ipc", id: "invoke-ipc",
instantiate: (di) => di.inject(ipcRendererInjectable).invoke, instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args);
},
}); });
export default invokeIpcInjectable; export default invokeIpcInjectable;

View File

@ -1,21 +0,0 @@
import { createContainer, DiContainer } from "@ogre-tools/injectable";
import { registerFeature } from "@k8slens/feature-core";
import { messagingFeatureForRenderer } from "../feature";
import { ipcRenderer } from "electron";
import invokeIpcInjectable from "./invoke-ipc.injectable";
describe("ipc-renderer", () => {
let di: DiContainer;
beforeEach(() => {
di = createContainer("irrelevant");
registerFeature(di, messagingFeatureForRenderer);
});
it("is IPC-renderer invoke of Electron", () => {
const actual = di.inject(invokeIpcInjectable);
expect(actual).toBe(ipcRenderer.invoke);
});
});

View File

@ -4,7 +4,11 @@ import ipcRendererInjectable from "../ipc/ipc-renderer.injectable";
const sendToIpcInjectable = getInjectable({ const sendToIpcInjectable = getInjectable({
id: "send-to-ipc", id: "send-to-ipc",
instantiate: (di) => di.inject(ipcRendererInjectable).send, instantiate: (di) => {
const ipcRenderer = di.inject(ipcRendererInjectable);
return (channel: string, ...args: unknown[]) => ipcRenderer.send(channel, ...args);
},
}); });
export default sendToIpcInjectable; export default sendToIpcInjectable;

View File

@ -1,21 +0,0 @@
import { createContainer, DiContainer } from "@ogre-tools/injectable";
import { registerFeature } from "@k8slens/feature-core";
import { messagingFeatureForRenderer } from "../feature";
import { ipcRenderer } from "electron";
import sendToIpcInjectable from "./send-to-ipc.injectable";
describe("ipc-renderer", () => {
let di: DiContainer;
beforeEach(() => {
di = createContainer("irrelevant");
registerFeature(di, messagingFeatureForRenderer);
});
it("is IPC-renderer send of Electron", () => {
const actual = di.inject(sendToIpcInjectable);
expect(actual).toBe(ipcRenderer.send);
});
});

View File

@ -16,7 +16,7 @@ import {
import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import { getPromiseStatus } from "@k8slens/test-utils"; import { getPromiseStatus } from "@k8slens/test-utils";
import { getMessageBridgeFake } from "./get-message-bridge-fake"; import { getMessageBridgeFake, MessageBridgeFake } from "./get-message-bridge-fake";
import { startApplicationInjectionToken } from "@k8slens/application"; import { startApplicationInjectionToken } from "@k8slens/application";
type SomeMessageChannel = MessageChannel<string>; type SomeMessageChannel = MessageChannel<string>;
@ -33,7 +33,7 @@ const someRequestChannelWithoutListeners: SomeRequestChannel = {
[{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(({ scenarioIsAsync }) => [{ scenarioIsAsync: true }, { scenarioIsAsync: false }].forEach(({ scenarioIsAsync }) =>
describe(`get-message-bridge-fake, given running as ${scenarioIsAsync ? "async" : "sync"}`, () => { describe(`get-message-bridge-fake, given running as ${scenarioIsAsync ? "async" : "sync"}`, () => {
let messageBridgeFake: any; let messageBridgeFake: MessageBridgeFake;
beforeEach(() => { beforeEach(() => {
messageBridgeFake = getMessageBridgeFake(); messageBridgeFake = getMessageBridgeFake();
@ -142,15 +142,14 @@ const someRequestChannelWithoutListeners: SomeRequestChannel = {
? "when all message steps are propagated using a wrapper" ? "when all message steps are propagated using a wrapper"
: "immediately"; : "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioTitle, () => { describe(scenarioTitle, () => {
let someWrapper: jest.Mock; let someWrapper: jest.MockedFunction<() => Promise<void>>;
beforeEach((done) => { beforeEach((done) => {
someWrapper = jest.fn((propagation) => propagation()); someWrapper = jest.fn().mockImplementation((propagation: () => Promise<void>) => propagation());
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagationRecursive(someWrapper).then(done); void messageBridgeFake.messagePropagationRecursive(someWrapper).then(done);
} else { } else {
done(); done();
} }
@ -163,21 +162,21 @@ const someRequestChannelWithoutListeners: SomeRequestChannel = {
}); });
}); });
scenarioIsAsync && if (scenarioIsAsync) {
it("the wrapper gets called with the both propagations", () => { it("the wrapper gets called with the both propagations", () => {
expect(someWrapper).toHaveBeenCalledTimes(2); expect(someWrapper).toHaveBeenCalledTimes(2);
}); });
}
}); });
const scenarioName: string = scenarioIsAsync const scenarioName: string = scenarioIsAsync
? "when all message steps are propagated not using a wrapper" ? "when all message steps are propagated not using a wrapper"
: "immediately"; : "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => { describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagationRecursive().then(done); void messageBridgeFake.messagePropagationRecursive().then(done);
} else { } else {
done(); done();
} }
@ -204,19 +203,19 @@ const someRequestChannelWithoutListeners: SomeRequestChannel = {
expect(someHandler1MockInDi1).not.toHaveBeenCalled(); expect(someHandler1MockInDi1).not.toHaveBeenCalled();
}); });
scenarioIsAsync && if (scenarioIsAsync) {
it("listeners in other than sending DIs do not handle the message yet", () => { it("listeners in other than sending DIs do not handle the message yet", () => {
expect(someHandler1MockInDi2).not.toHaveBeenCalled(); expect(someHandler1MockInDi2).not.toHaveBeenCalled();
expect(someHandler2MockInDi2).not.toHaveBeenCalled(); expect(someHandler2MockInDi2).not.toHaveBeenCalled();
}); });
}
const scenarioName = scenarioIsAsync ? "when messages are propagated" : "immediately"; const scenarioName = scenarioIsAsync ? "when messages are propagated" : "immediately";
// eslint-disable-next-line jest/valid-title
describe(scenarioName, () => { describe(scenarioName, () => {
beforeEach((done) => { beforeEach((done) => {
if (scenarioIsAsync) { if (scenarioIsAsync) {
messageBridgeFake.messagePropagation().then(done); void messageBridgeFake.messagePropagation().then(done);
} else { } else {
done(); done();
} }
@ -235,12 +234,12 @@ const someRequestChannelWithoutListeners: SomeRequestChannel = {
}); });
}); });
scenarioIsAsync && if (scenarioIsAsync) {
describe("when messages are propagated using a wrapper, such as act() in react testing lib", () => { describe("when messages are propagated using a wrapper, such as act() in react testing lib", () => {
let someWrapper: jest.Mock; let someWrapper: jest.MockedFunction<() => Promise<void>>;
beforeEach(async () => { beforeEach(async () => {
someWrapper = jest.fn((observation) => observation()); someWrapper = jest.fn().mockImplementation((propagation: () => Promise<void>) => propagation());
await messageBridgeFake.messagePropagation(someWrapper); await messageBridgeFake.messagePropagation(someWrapper);
}); });
@ -261,6 +260,7 @@ const someRequestChannelWithoutListeners: SomeRequestChannel = {
}); });
}); });
}); });
}
}); });
it("given a listener is deregistered, when sending message, deregistered listener does not handle the message", () => { it("given a listener is deregistered, when sending message, deregistered listener does not handle the message", () => {

View File

@ -5,6 +5,7 @@ import type {
MessageChannelListener, MessageChannelListener,
RequestChannel, RequestChannel,
RequestChannelHandler, RequestChannelHandler,
RequestChannelListener,
} from "@k8slens/messaging"; } from "@k8slens/messaging";
import { import {
@ -21,27 +22,26 @@ import asyncFn, { AsyncFnMock } from "@async-fn/jest";
export type MessageBridgeFake = { export type MessageBridgeFake = {
involve: (...dis: DiContainer[]) => void; involve: (...dis: DiContainer[]) => void;
messagePropagation: () => Promise<void>; messagePropagation: (callback?: () => Promise<void>) => Promise<void>;
messagePropagationRecursive: (callback: () => any) => any; messagePropagationRecursive: (callback?: () => Promise<void>) => Promise<void>;
setAsync: (value: boolean) => void; setAsync: (value: boolean) => void;
}; };
type MessageHandlers = Set<MessageChannelHandler<MessageChannel<any>>>; type MessageHandlers = Set<MessageChannelHandler<MessageChannel<unknown>>>;
interface OverrideMessagingArgs {
di: DiContainer;
messageListenersByDi: Map<DiContainer, Map<string, MessageHandlers>>;
messagePropagationBuffer: Set<AsyncFnMock<() => Promise<void>>>;
getAsyncModeStatus: () => boolean;
}
const overrideMessaging = ({ const overrideMessaging = ({
di, di,
messageListenersByDi, messageListenersByDi,
messagePropagationBuffer, messagePropagationBuffer,
getAsyncModeStatus, getAsyncModeStatus,
}: { }: OverrideMessagingArgs) => {
di: DiContainer;
messageListenersByDi: Map<DiContainer, Map<string, MessageHandlers>>;
messagePropagationBuffer: Set<{ resolve: () => Promise<void> }>;
getAsyncModeStatus: () => boolean;
}) => {
const messageHandlersByChannel = new Map<string, MessageHandlers>(); const messageHandlersByChannel = new Map<string, MessageHandlers>();
messageListenersByDi.set(di, messageHandlersByChannel); messageListenersByDi.set(di, messageHandlersByChannel);
@ -59,9 +59,9 @@ const overrideMessaging = ({
} }
if (getAsyncModeStatus()) { if (getAsyncModeStatus()) {
const resolvableHandlePromise = asyncFn(); const resolvableHandlePromise = asyncFn<() => Promise<void>>();
resolvableHandlePromise().then(() => { void resolvableHandlePromise().then(() => {
handlersForChannel.forEach((handler) => handler(message, { frameId: 42, processId: 42 })); handlersForChannel.forEach((handler) => handler(message, { frameId: 42, processId: 42 }));
}); });
@ -74,24 +74,23 @@ const overrideMessaging = ({
di.override( di.override(
enlistMessageChannelListenerInjectionToken, enlistMessageChannelListenerInjectionToken,
() => () => (listener: MessageChannelListener<MessageChannel<unknown>>) => {
<T>(listener: MessageChannelListener<MessageChannel<T>>) => { if (!messageHandlersByChannel.has(listener.channel.id)) {
if (!messageHandlersByChannel.has(listener.channel.id)) { messageHandlersByChannel.set(listener.channel.id, new Set());
messageHandlersByChannel.set(listener.channel.id, new Set()); }
}
const handlerSet = messageHandlersByChannel.get(listener.channel.id); const handlerSet = messageHandlersByChannel.get(listener.channel.id);
handlerSet?.add(listener.handler); handlerSet?.add(listener.handler);
return () => { return () => {
handlerSet?.delete(listener.handler); handlerSet?.delete(listener.handler);
}; };
}, },
); );
}; };
type RequestHandlers = Set<RequestChannelHandler<RequestChannel<any, any>>>; type RequestHandlers = Set<RequestChannelHandler<RequestChannel<unknown, unknown>>>;
const overrideRequesting = ({ const overrideRequesting = ({
di, di,
@ -128,9 +127,7 @@ const overrideRequesting = ({
} }
const listeners = channelSpecificListeners[0]; const listeners = channelSpecificListeners[0];
const [handler] = listeners ?? [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [handler] = listeners!;
return handler; return handler;
}, },
@ -139,35 +136,41 @@ const overrideRequesting = ({
)) as RequestFromChannel, )) as RequestFromChannel,
); );
di.override(enlistRequestChannelListenerInjectionToken, () => (listener) => { di.override(
if (!requestHandlersByChannel.has(listener.channel.id)) { enlistRequestChannelListenerInjectionToken,
requestHandlersByChannel.set(listener.channel.id, new Set()); () => (listener: RequestChannelListener<RequestChannel<unknown, unknown>>) => {
} if (!requestHandlersByChannel.has(listener.channel.id)) {
requestHandlersByChannel.set(listener.channel.id, new Set());
}
const handlerSet = requestHandlersByChannel.get(listener.channel.id); const handlerSet = requestHandlersByChannel.get(listener.channel.id);
handlerSet?.add(listener.handler); handlerSet?.add(listener.handler);
return () => { return () => {
handlerSet?.delete(listener.handler); handlerSet?.delete(listener.handler);
}; };
}); },
);
}; };
export const getMessageBridgeFake = (): MessageBridgeFake => { export const getMessageBridgeFake = (): MessageBridgeFake => {
const messageListenersByDi = new Map<DiContainer, Map<string, MessageHandlers>>(); const messageListenersByDi = new Map<DiContainer, Map<string, MessageHandlers>>();
const requestListenersByDi = new Map<DiContainer, Map<string, RequestHandlers>>(); const requestListenersByDi = new Map<DiContainer, Map<string, RequestHandlers>>();
const messagePropagationBuffer = new Set<AsyncFnMock<() => Promise<void>>>();
const messagePropagationBuffer = new Set<AsyncFnMock<() => void>>(); const messagePropagation = async (
wrapper: (callback: () => Promise<void>) => Promise<void> = (callback) => callback(),
const messagePropagation = async (wrapper: (callback: any) => any = (callback) => callback()) => { ) => {
const oldMessages = [...messagePropagationBuffer.values()]; const oldMessages = [...messagePropagationBuffer.values()];
messagePropagationBuffer.clear(); messagePropagationBuffer.clear();
await Promise.all(oldMessages.map((x) => wrapper(x.resolve))); await Promise.all(oldMessages.map((x) => wrapper(x.resolve)));
}; };
const messagePropagationRecursive = async (wrapper = (callback: () => any) => callback()) => { const messagePropagationRecursive = async (
wrapper: (callback: () => Promise<void>) => Promise<void> = (callback) => callback(),
) => {
while (messagePropagationBuffer.size) { while (messagePropagationBuffer.size) {
await messagePropagation(wrapper); await messagePropagation(wrapper);
} }

View File

@ -4,12 +4,12 @@
*/ */
import "./button.scss"; import "./button.scss";
import type { ButtonHTMLAttributes } from "react"; import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react";
import React from "react"; import React from "react";
import { cssNames, StrictReactNode } from "@k8slens/utilities"; import { cssNames, StrictReactNode } from "@k8slens/utilities";
import { withTooltip } from "@k8slens/tooltip"; import { withTooltip } from "@k8slens/tooltip";
export interface ButtonProps extends ButtonHTMLAttributes<any> { export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
label?: StrictReactNode; label?: StrictReactNode;
waiting?: boolean; waiting?: boolean;
primary?: boolean; primary?: boolean;
@ -21,7 +21,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<any> {
active?: boolean; active?: boolean;
big?: boolean; big?: boolean;
round?: boolean; round?: boolean;
href?: string; // render as hyperlink href?: string;
target?: "_blank"; // in case of using @href target?: "_blank"; // in case of using @href
children?: StrictReactNode; children?: StrictReactNode;
} }
@ -46,19 +46,21 @@ export const Button = withTooltip((props: ButtonProps) => {
light, light,
}); });
// render as link
if (props.href) { if (props.href) {
return ( return (
<a {...btnProps}> <a {...(btnProps as AnchorHTMLAttributes<HTMLAnchorElement>)}>
{label} {label}
{children} {children}
</a> </a>
); );
} }
// render as button
return ( return (
<button type="button" {...btnProps} data-waiting={typeof waiting === "boolean" ? String(waiting) : undefined}> <button
type="button"
{...(btnProps as ButtonHTMLAttributes<HTMLButtonElement>)}
data-waiting={typeof waiting === "boolean" ? String(waiting) : undefined}
>
{label} {label}
{children} {children}
</button> </button>

View File

@ -323,7 +323,7 @@ describe("computeNextPosition technical tests", () => {
it("doesn't throw if the preferredPosition is invalid", () => { it("doesn't throw if the preferredPosition is invalid", () => {
expect( expect(
computeNextPosition({ computeNextPosition({
preferredPositions: "some-invalid-data" as any, preferredPositions: "some-invalid-data" as unknown as TooltipPosition,
offset: 10, offset: 10,
target: { target: {
getBoundingClientRect: () => getBoundingClientRect: () =>

View File

@ -11,7 +11,6 @@ import { observer } from "mobx-react";
import type { IClassName, StrictReactNode } from "@k8slens/utilities"; import type { IClassName, StrictReactNode } from "@k8slens/utilities";
import { cssNames } from "@k8slens/utilities"; import { cssNames } from "@k8slens/utilities";
import { observable, makeObservable, action, runInAction } from "mobx"; import { observable, makeObservable, action, runInAction } from "mobx";
import autoBindReact from "auto-bind/react";
import { computeNextPosition } from "./helpers"; import { computeNextPosition } from "./helpers";
export enum TooltipPosition { export enum TooltipPosition {
@ -67,7 +66,6 @@ class DefaultedTooltip extends React.Component<TooltipProps & typeof defaultProp
constructor(props: TooltipProps & typeof defaultProps) { constructor(props: TooltipProps & typeof defaultProps) {
super(props); super(props);
makeObservable(this); makeObservable(this);
autoBindReact(this);
} }
get targetElem(): HTMLElement | null { get targetElem(): HTMLElement | null {
@ -97,17 +95,15 @@ class DefaultedTooltip extends React.Component<TooltipProps & typeof defaultProp
this.hoverTarget?.removeEventListener("mouseleave", this.onLeaveTarget); this.hoverTarget?.removeEventListener("mouseleave", this.onLeaveTarget);
} }
@action onEnterTarget = action(() => {
protected onEnterTarget() {
this.isVisible = true; this.isVisible = true;
requestAnimationFrame(action(() => (this.isContentVisible = true))); requestAnimationFrame(action(() => (this.isContentVisible = true)));
} });
@action onLeaveTarget = action(() => {
protected onLeaveTarget() {
this.isVisible = false; this.isVisible = false;
this.isContentVisible = false; this.isContentVisible = false;
} });
refreshPosition() { refreshPosition() {
const { preferredPositions, offset } = this.props; const { preferredPositions, offset } = this.props;

View File

@ -49,10 +49,12 @@ export function withTooltip<TargetProps>(
); );
} }
const ResolvedTarget = Target as React.FunctionComponent<TargetProps>;
return ( return (
<Target id={targetId} {...(targetProps as any)}> <ResolvedTarget id={targetId} {...(targetProps as TargetProps)}>
{targetChildren} {targetChildren}
</Target> </ResolvedTarget>
); );
}; };

View File

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getOrInsert } from "./collection-functions";
export type Falsy = false | 0 | "" | null | undefined; export type Falsy = false | 0 | "" | null | undefined;
interface Iterator<T> extends Iterable<T> { interface Iterator<T> extends Iterable<T> {
@ -11,7 +13,8 @@ interface Iterator<T> extends Iterable<T> {
find(fn: (val: T) => unknown): T | undefined; find(fn: (val: T) => unknown): T | undefined;
collect<U>(fn: (values: Iterable<T>) => U): U; collect<U>(fn: (values: Iterable<T>) => U): U;
toArray(): T[]; toArray(): T[];
toMap(): T extends readonly [infer K, infer V] ? Map<K, V[]> : never; toMap(): T extends readonly [infer K, infer V] ? Map<K, V> : never;
groupIntoMap(): T extends readonly [infer K, infer V] ? Map<K, V[]> : never;
toSet(): Set<T>; toSet(): Set<T>;
map<U>(fn: (val: T) => U): Iterator<U>; map<U>(fn: (val: T) => U): Iterator<U>;
flatMap<U>(fn: (val: T) => U[]): Iterator<U>; flatMap<U>(fn: (val: T) => U[]): Iterator<U>;
@ -30,21 +33,15 @@ function chain<T>(src: IterableIterator<T>): Iterator<T> {
join: (sep) => join(src, sep), join: (sep) => join(src, sep),
collect: (fn) => fn(src), collect: (fn) => fn(src),
toArray: () => [...src], toArray: () => [...src],
toMap: () => { toMap: () => new Map(src as IterableIterator<[any, any]>) as any,
const res = new Map(); groupIntoMap: () => {
const res = new Map() as T extends readonly [infer K, infer V] ? Map<K, V[]> : never;
for (const item of src[Symbol.iterator]()) { for (const [key, value] of src as IterableIterator<[any, any]>) {
const [key, val] = item as [unknown, unknown]; getOrInsert(res, key, []).push(value);
const existing = res.get(key);
if (existing) {
existing.push(val);
} else {
res.set(key, [val]);
}
} }
return res as T extends readonly [infer K, infer V] ? Map<K, V[]> : never; return res;
}, },
toSet: () => new Set(src), toSet: () => new Set(src),
concat: (src2) => chain(concat(src, src2)), concat: (src2) => chain(concat(src, src2)),