chore: add extensions.customExtensions.enabled check for install custom extensions button (#16296)

* chore: add extensions.customExtensions.enabled check for install custom extensions button
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: add documentation
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: update according to reviews and fix tests
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: apply comments
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: update tests
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: apply comments
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: fix typo
Signed-off-by: Sonia Sandler <ssandler@redhat.com>
This commit is contained in:
Sonia Sandler 2026-03-09 16:13:23 -04:00 committed by GitHub
parent ab591ca11d
commit 7a93b8bd3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 96 additions and 29 deletions

View file

@ -388,10 +388,25 @@ export class ExtensionLoader implements IAsyncDisposable {
},
};
const allowCustomExtensions: IConfigurationNode = {
id: 'preferences.extensions',
title: 'Extensions',
type: 'object',
properties: {
['extensions.customExtensions.enabled']: {
description: 'When disabled, the `Install custom...` button on the Extensions page will not appear.',
type: 'boolean',
default: true,
hidden: true,
},
},
};
this.configurationRegistry.registerConfigurations([
maxActivationTimeConfiguration,
disabledExtensionConfiguration,
developmentModeExtensionConfiguration,
allowCustomExtensions,
]);
}

View file

@ -30,6 +30,7 @@ import ExtensionList from './ExtensionList.svelte';
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
});
export const aFakeExtension: CatalogExtension = {
@ -100,8 +101,10 @@ test('Expect to see extensions', async () => {
render(ExtensionList);
const headingExtensions = screen.getByRole('heading', { name: 'extensions' });
expect(headingExtensions).toBeInTheDocument();
await vi.waitFor(() => {
const headingExtensions = screen.getByRole('heading', { name: 'extensions' });
expect(headingExtensions).toBeInTheDocument();
});
// get first extension
const myExtension1 = screen.getByRole('region', { name: 'idAInstalled' });
@ -126,8 +129,12 @@ test('Expect to see empty screen on extension page only', async () => {
render(ExtensionList, { searchTerm: 'A' });
let title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).toBeInTheDocument();
let title: HTMLElement | null;
await vi.waitFor(() => {
title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).toBeInTheDocument();
});
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
@ -143,12 +150,16 @@ test('Expect to see empty screen on catalog page only', async () => {
render(ExtensionList, { searchTerm: 'A' });
let title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).not.toBeInTheDocument();
let title: HTMLElement | null;
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
await vi.waitFor(async () => {
title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).not.toBeInTheDocument();
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
});
title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).toBeInTheDocument();
@ -160,8 +171,12 @@ test('Expect to see empty screens on both pages', async () => {
render(ExtensionList, { searchTerm: 'foo' });
let title = screen.getByText(`No extensions matching 'foo' found`);
expect(title).toBeInTheDocument();
let title: HTMLElement | null;
await vi.waitFor(() => {
title = screen.getByText(`No extensions matching 'foo' found`);
expect(title).toBeInTheDocument();
});
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
@ -177,8 +192,10 @@ test('Search extension page searches also description', async () => {
render(ExtensionList, { searchTerm: 'bar' });
const myExtension1 = screen.getByRole('region', { name: 'idAInstalled' });
expect(myExtension1).toBeInTheDocument();
await vi.waitFor(() => {
const myExtension1 = screen.getByRole('region', { name: 'idAInstalled' });
expect(myExtension1).toBeInTheDocument();
});
// second extension should not be there as only in catalog (not installed) and doesn't have "bar" in the description
const extensionIdB = screen.queryByRole('group', { name: 'B Extension' });
@ -200,9 +217,11 @@ test('Search catalog page searches also description', async () => {
render(ExtensionList, { searchTerm: 'bar' });
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
await vi.waitFor(async () => {
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
});
// Verify that the extension containing "bar" in the description is displayed
const myExtension1 = screen.getByRole('group', { name: 'A Extension' });
@ -217,11 +236,15 @@ test('Expect to see local extensions tab content', async () => {
catalogExtensionInfos.set([]);
extensionInfos.set([]);
vi.mocked(window.getConfigurationValue).mockResolvedValue(undefined);
render(ExtensionList);
// select the local extensions tab
const localModeTab = screen.getByRole('button', { name: 'Local Extensions' });
await fireEvent.click(localModeTab);
await vi.waitFor(async () => {
// select the local extensions tab
const localModeTab = screen.getByRole('button', { name: 'Local Extensions' });
await fireEvent.click(localModeTab);
});
// expect to see empty screen
const emptyText = screen.getByText('Enable Preferences > Extensions > Development Mode to test local extensions');
@ -234,12 +257,34 @@ test('Switching tabs keeps only terms in search term', async () => {
render(ExtensionList, { searchTerm: 'bar category:bar not:installed' });
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
await vi.waitFor(async () => {
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
});
// Verify that the extension containing "bar" in the description is displayed (which is not in bar category and is installed)
// meaning that `category:bar not:installed` has been removed from search term
const myExtension1 = screen.getByRole('group', { name: 'A Extension' });
expect(myExtension1).toBeInTheDocument();
});
test('Expect install custom button is visible', async () => {
render(ExtensionList);
await vi.waitFor(() => {
const installCustomButton = screen.getByRole('button', { name: 'Install custom' });
expect(installCustomButton).toBeInTheDocument();
});
});
test('Expect install custom button to not be visible if extensions.customExtensions.enabled is false', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(false);
render(ExtensionList);
await vi.waitFor(() => {
const installCustomButton = screen.queryByRole('button', { name: 'Install custom' });
expect(installCustomButton).not.toBeInTheDocument();
});
});

View file

@ -24,6 +24,10 @@ let { searchTerm = '', screen = 'installed' }: Props = $props();
const extensionsUtils = new ExtensionsUtils();
let enableCustomExtensions = $derived(
(await window.getConfigurationValue('extensions.customExtensions.enabled')) ?? true,
);
const filteredInstalledExtensions: CombinedExtensionInfoUI[] = $derived(
extensionsUtils.filterInstalledExtensions($combinedInstalledExtensions, searchTerm),
);
@ -63,13 +67,15 @@ function changeScreen(newScreen: 'installed' | 'catalog' | 'development'): void
<NavPage bind:searchTerm={searchTerm} title="extensions">
{#snippet additionalActions()}
<Button
on:click={(): void => {
installManualImageModal = true;
}}
icon={faCloudDownload}
title="Install manually an extension"
aria-label="Install custom">Install custom...</Button>
{#if enableCustomExtensions}
<Button
on:click={(): void => {
installManualImageModal = true;
}}
icon={faCloudDownload}
title="Install manually an extension"
aria-label="Install custom">Install custom...</Button>
{/if}
{/snippet}
{#snippet bottomAdditionalActions()}

View file

@ -49,6 +49,7 @@ import TabItem from '@theme/TabItem';
| `editor.integrated.fontSize` | number | `10` | Editor font size (6-100 px) |
| `extensions.autoCheckUpdates` | boolean | `true` | Auto-check for extension updates |
| `extensions.autoUpdate` | boolean | `true` | Auto-install extension updates |
| `extensions.customExtensions.enabled` | boolean | `true` | When disabled, the `Install custom...` button on the Extensions page will not appear. |
| `extensions.ignoreBannerRecommendations` | boolean | `false` | Disable recommendation banners |
| `extensions.ignoreRecommendations` | boolean | `false` | Disable extension recommendations |
| `feedback.dialog` | boolean | `true` | Show experimental feature feedback dialog |