diff --git a/src/features/preferences/__snapshots__/hiding-of-empty-branches.test.tsx.snap b/src/features/preferences/__snapshots__/hiding-of-empty-branches.test.tsx.snap
new file mode 100644
index 0000000000..320b19728f
--- /dev/null
+++ b/src/features/preferences/__snapshots__/hiding-of-empty-branches.test.tsx.snap
@@ -0,0 +1,2020 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`preferences - hiding-of-empty-branches, given in preferences page given tab group and empty tabs renders 1`] = `
+
+
+
+
+
+
+
+ home
+
+
+
+
+
+
+
+ arrow_back
+
+
+
+
+
+
+
+ arrow_forward
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Application
+
+
+
+
+
+ Extension Install Registry
+
+
+
+
+ This setting is to change the registry URL for installing extensions by name.
+ If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
+
+ .npmrc
+
+ file or in the input below.
+
+
+
+
+
+
+
+
+ Update Channel
+
+
+
+
+
+
+
+ Locale Timezone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ arrow_left
+
+
+
+
+
+ arrow_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`preferences - hiding-of-empty-branches, given in preferences page given tab group and empty tabs when an item appears for one of the tabs renders 1`] = `
+
+
+
+
+
+
+
+ home
+
+
+
+
+
+
+
+ arrow_back
+
+
+
+
+
+
+
+ arrow_forward
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Application
+
+
+
+
+
+ Extension Install Registry
+
+
+
+
+ This setting is to change the registry URL for installing extensions by name.
+ If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
+
+ .npmrc
+
+ file or in the input below.
+
+
+
+
+
+
+
+
+ Update Channel
+
+
+
+
+
+
+
+ Locale Timezone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ arrow_left
+
+
+
+
+
+ arrow_right
+
+
+
+
+
+
+
+
+`;
+
+exports[`preferences - hiding-of-empty-branches, given in preferences page given tab group and empty tabs when an item appears for one of the tabs when an item appears for the remaining tab renders 1`] = `
+
+
+
+
+
+
+
+ home
+
+
+
+
+
+
+
+ arrow_back
+
+
+
+
+
+
+
+ arrow_forward
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Application
+
+
+
+
+
+ Extension Install Registry
+
+
+
+
+ This setting is to change the registry URL for installing extensions by name.
+ If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
+
+ .npmrc
+
+ file or in the input below.
+
+
+
+
+
+
+
+
+ Update Channel
+
+
+
+
+
+
+
+ Locale Timezone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ arrow_left
+
+
+
+
+
+ arrow_right
+
+
+
+
+
+
+
+
+`;
diff --git a/src/features/preferences/hiding-of-empty-branches.test.tsx b/src/features/preferences/hiding-of-empty-branches.test.tsx
new file mode 100644
index 0000000000..9c36b1eaab
--- /dev/null
+++ b/src/features/preferences/hiding-of-empty-branches.test.tsx
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import React from "react";
+import type { DiContainer } from "@ogre-tools/injectable";
+import { getInjectable } from "@ogre-tools/injectable";
+import type { RenderResult } from "@testing-library/react";
+import { runInAction } from "mobx";
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { preferenceItemInjectionToken } from "./renderer/preference-items/preference-item-injection-token";
+
+describe("preferences - hiding-of-empty-branches, given in preferences page", () => {
+ let builder: ApplicationBuilder;
+ let rendered: RenderResult;
+ let windowDi: DiContainer;
+
+ beforeEach(async () => {
+ builder = getApplicationBuilder();
+
+ rendered = await builder.render();
+
+ builder.preferences.navigate();
+
+ windowDi = builder.applicationWindow.only.di;
+ });
+
+ describe("given tab group and empty tabs", () => {
+ beforeEach(() => {
+ const someTabGroupInjectable = getInjectable({
+ id: "some-tab-group",
+
+ instantiate: () => ({
+ kind: "tab-group" as const,
+ id: "some-tab-group",
+ testId: "some-tab-group-test-id",
+ parentId: "preference-tabs" as const,
+ label: "Some tab group label",
+ orderNumber: 10,
+ }),
+
+ injectionToken: preferenceItemInjectionToken,
+ });
+
+ const tabWithItemsInjectable = getInjectable({
+ id: "some-tab",
+
+ instantiate: () => ({
+ kind: "tab" as const,
+ id: "some-tab-with-items-id",
+ parentId: "some-tab-group" as const,
+ testId: "some-tab-with-items",
+ pathId: "irrelevant",
+ label: "Some label for tab with items",
+ orderNumber: 10,
+ }),
+
+ injectionToken: preferenceItemInjectionToken,
+ });
+
+ const tabWithoutItemsInjectable = getInjectable({
+ id: "some-empty-tab",
+
+ instantiate: () => ({
+ kind: "tab" as const,
+ id: "some-tab-without-items-id",
+ parentId: "some-tab-group" as const,
+ testId: "some-tab-without-items",
+ pathId: "irrelevant",
+ label: "Some label for tab without items",
+ orderNumber: 10,
+ }),
+
+ injectionToken: preferenceItemInjectionToken,
+ });
+
+ runInAction(() => {
+ windowDi.register(
+ someTabGroupInjectable,
+ tabWithItemsInjectable,
+ tabWithoutItemsInjectable,
+ );
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.container).toMatchSnapshot();
+ });
+
+ it("does not render the empty tab group", () => {
+ const someTabGroup = rendered.queryByTestId("some-tab-group-test-id");
+
+ expect(someTabGroup).toBeNull();
+ });
+
+ it("does not render the empty tabs", () => {
+ const someTab = rendered.queryByTestId("some-tab-with-items");
+ const someOtherTab = rendered.queryByTestId("some-tab-without-items");
+
+ expect([someTab, someOtherTab]).toEqual([null, null]);
+ });
+
+ describe("when an item appears for one of the tabs", () => {
+ beforeEach(() => {
+ const itemForTabInjectable = getInjectable({
+ id: "some-preference-item",
+
+ instantiate: () => ({
+ kind: "item" as const,
+ id: "some-preference-item-id",
+ parentId: "some-tab-with-items-id" as const,
+ testId: "some-preference-item",
+ Component: () => Irrelevant
,
+ orderNumber: 10,
+ }),
+
+ injectionToken: preferenceItemInjectionToken,
+ });
+
+ runInAction(() => {
+ windowDi.register(itemForTabInjectable);
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.container).toMatchSnapshot();
+ });
+
+ it("renders the tab group that is no longer empty", () => {
+ const someTabGroup = rendered.queryByTestId("some-tab-group-test-id");
+
+ expect(someTabGroup).not.toBeNull();
+ });
+
+ it("renders the tab that is no longer empty", () => {
+ const someTab = rendered.queryByTestId("some-tab-with-items");
+
+ expect(someTab).not.toBeNull();
+ });
+
+ it("does not render the tab that is still empty", () => {
+ const someTab = rendered.queryByTestId("some-tab-without-items");
+
+ expect(someTab).toBeNull();
+ });
+
+ describe("when an item appears for the remaining tab", () => {
+ beforeEach(() => {
+ const itemForTabInjectable = getInjectable({
+ id: "some-other-preference-item",
+
+ instantiate: () => ({
+ kind: "item" as const,
+ id: "some-other-preference-item-id",
+ parentId: "some-tab-without-items-id" as const,
+ testId: "some-other-preference-item",
+ Component: () => Irrelevant
,
+ orderNumber: 10,
+ }),
+
+ injectionToken: preferenceItemInjectionToken,
+ });
+
+ runInAction(() => {
+ windowDi.register(itemForTabInjectable);
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.container).toMatchSnapshot();
+ });
+
+ it("still renders the tab group that is not empty", () => {
+ const someTabGroup = rendered.queryByTestId("some-tab-group-test-id");
+
+ expect(someTabGroup).not.toBeNull();
+ });
+
+ it("still renders the tab that is not empty", () => {
+ const someTab = rendered.queryByTestId("some-tab-with-items");
+
+ expect(someTab).not.toBeNull();
+ });
+
+ it("now renders the other tab that is no longer empty", () => {
+ const someTab = rendered.queryByTestId("some-tab-without-items");
+
+ expect(someTab).not.toBeNull();
+ });
+ });
+ });
+ });
+});
diff --git a/src/features/preferences/renderer/preference-items/extensions-preference-tab-group.injectable.ts b/src/features/preferences/renderer/preference-items/extensions-preference-tab-group.injectable.ts
index 412ad3a371..4609fc0906 100644
--- a/src/features/preferences/renderer/preference-items/extensions-preference-tab-group.injectable.ts
+++ b/src/features/preferences/renderer/preference-items/extensions-preference-tab-group.injectable.ts
@@ -11,6 +11,7 @@ const extensionsPreferenceTabGroupInjectable = getInjectable({
instantiate: () => ({
kind: "tab-group" as const,
id: "extensions-tab-group",
+ testId: "extensions-tab-group",
parentId: "preference-tabs" as const,
label: "Extensions",
orderNumber: 20,
diff --git a/src/features/preferences/renderer/preference-items/general-preference-tab-group.injectable.ts b/src/features/preferences/renderer/preference-items/general-preference-tab-group.injectable.ts
index fe46026aa8..946f34d78a 100644
--- a/src/features/preferences/renderer/preference-items/general-preference-tab-group.injectable.ts
+++ b/src/features/preferences/renderer/preference-items/general-preference-tab-group.injectable.ts
@@ -11,6 +11,7 @@ const generalPreferenceTabGroupInjectable = getInjectable({
instantiate: () => ({
kind: "tab-group" as const,
id: "general-tab-group",
+ testId: "general-tab-group",
parentId: "preference-tabs" as const,
label: "Preferences",
orderNumber: 10,
diff --git a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts
index 0a2e78a92f..6662a6e645 100644
--- a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts
+++ b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts
@@ -22,6 +22,7 @@ export interface PreferenceTabGroup {
kind: "tab-group";
id: string;
parentId: "preference-tabs";
+ testId: string;
label: string;
orderNumber: number;
isShown?: boolean;
diff --git a/src/features/preferences/renderer/preference-navigation/preferences-navigation-tab.tsx b/src/features/preferences/renderer/preference-navigation/preferences-navigation-tab.tsx
index 23b908b50d..62a6573874 100644
--- a/src/features/preferences/renderer/preference-navigation/preferences-navigation-tab.tsx
+++ b/src/features/preferences/renderer/preference-navigation/preferences-navigation-tab.tsx
@@ -23,7 +23,7 @@ interface PreferenceNavigationTabProps {
const NonInjectedPreferencesNavigationTab = observer(({ navigateToTab, tabIsActive, tab } : Dependencies & PreferenceNavigationTabProps) => (
navigateToTab(tab.pathId)}
- data-testid={`tab-link-for-${tab.pathId}`}
+ data-testid={tab.testId}
active={tabIsActive.get()}
label={tab.label}
/>
diff --git a/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx b/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx
index 73f2e39596..e1bc94faf8 100644
--- a/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx
+++ b/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx
@@ -12,6 +12,7 @@ import type { IComputedValue } from "mobx";
import preferencesCompositeInjectable from "../preference-items/preferences-composite.injectable";
import { observer } from "mobx-react";
import { PreferencesNavigationTab } from "./preferences-navigation-tab";
+import { compositeHasDescendant } from "../../../application-menu/main/menu-items/get-composite/composite-has-descendant/composite-has-descendant";
interface Dependencies {
composite: IComputedValue>;
@@ -35,21 +36,34 @@ export const PreferencesNavigation = withInjectables(
const toNavigationHierarchy = (composite: Composite) => {
+ // Note: This makes tab groups and tabs without content not render anything in navigation.
+ if (!hasContent(composite)) {
+ return emptyRender;
+ }
+
const value = composite.value;
switch (value.kind) {
- case "page":
- case "item":
+ // Note: These preference item types are not rendered in navigation,
+ // yet they are interesting for deciding if eg. a tab group or a tab has content
+ // somewhere in structure, and therefore not be hidden.
+ case "page": {
+ return emptyRender;
+ }
+
+ case "item": {
+ return emptyRender;
+ }
- // eslint-disable-next-line no-fallthrough
case "group": {
- throw new Error("Should never come here");
+ return emptyRender;
}
case "tab-group": {
+
return (
<>
- {value.label}
+ {value.label}
>
@@ -76,3 +90,10 @@ const toNavigationHierarchy = (composite: Composite) => {
}
}
};
+
+const hasContent = compositeHasDescendant(
+ (composite) => composite.value.kind === "item",
+);
+
+const emptyRender = <>>;
+