Tweak modules to be disabled if config is missing (#33806)
* Consolidate modules vitest coverage * Use vite-common as base for modules vitest config * Make knip happier * Fix coverage paths * Place modules unit tests alongside src * Switch to defineProject for better type safety * Consolidate vitest CI & coverage Kills off vite-common * Update comment * Update lockfile * Fix shared-components vitest config * Soften eslint config for tests in modules * Run eslint on modules/playwright dir too * Make tsc happy * Tweak modules to be disabled if config is missing * Restore blank line * Improve coverage * Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
parent
d92e10f01a
commit
44c540eca0
@@ -35,12 +35,28 @@ test.describe("Banner", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should error if config is missing", async ({ page }) => {
|
test("should not error if config is missing", async ({ page }) => {
|
||||||
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
|
await expect(page.getByText("Your Element is misconfigured")).not.toBeVisible();
|
||||||
await expect(page.getByText("Errors in module configuration")).toBeVisible();
|
await expect(page.getByText("Errors in module configuration")).not.toBeVisible();
|
||||||
// We don't take a screenshot as we don't want to assert Element's styling, only our own
|
// We don't take a screenshot as we don't want to assert Element's styling, only our own
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("invalid config", () => {
|
||||||
|
test.use({
|
||||||
|
config: {
|
||||||
|
"io.element.element-web-modules.banner": {
|
||||||
|
type: "invalid",
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render error", async ({ page }) => {
|
||||||
|
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
|
||||||
|
await expect(page.getByText("Errors in module configuration")).toBeVisible();
|
||||||
|
// We don't take a screenshot as we don't want to assert Element's styling, only our own
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const configs: input<ConfigSchema>[] = [
|
const configs: input<ConfigSchema>[] = [
|
||||||
{
|
{
|
||||||
logo_url: "http://localhost:8080/logo.svg",
|
logo_url: "http://localhost:8080/logo.svg",
|
||||||
|
|||||||
@@ -115,6 +115,6 @@ export const CONFIG_KEY = "io.element.element-web-modules.banner";
|
|||||||
|
|
||||||
declare module "@element-hq/element-web-module-api" {
|
declare module "@element-hq/element-web-module-api" {
|
||||||
export interface Config {
|
export interface Config {
|
||||||
[CONFIG_KEY]: input<ConfigSchema>;
|
[CONFIG_KEY]?: input<ConfigSchema>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { type Api } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
|
import BannerModule from "./index";
|
||||||
|
|
||||||
|
const makeApi = (config: unknown): Api => {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
get: vi.fn().mockReturnValue(config),
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
register: vi.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Api;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("BannerModule", () => {
|
||||||
|
describe("load", () => {
|
||||||
|
it("should do nothing if no config present", async () => {
|
||||||
|
const api = makeApi(null);
|
||||||
|
|
||||||
|
const module = new BannerModule(api);
|
||||||
|
await module.load();
|
||||||
|
|
||||||
|
expect(api.i18n.register).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,13 +23,19 @@ class BannerModule implements Module {
|
|||||||
public constructor(private api: Api) {}
|
public constructor(private api: Api) {}
|
||||||
|
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
|
const rawConfig = this.api.config.get(CONFIG_KEY);
|
||||||
|
if (!rawConfig) {
|
||||||
|
console.debug(`No configuration found for module "${ModuleName}", skipping initialization.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.adoptedStyleSheets.push(compound);
|
document.adoptedStyleSheets.push(compound);
|
||||||
document.adoptedStyleSheets.push(style);
|
document.adoptedStyleSheets.push(style);
|
||||||
|
|
||||||
this.api.i18n.register(Translations);
|
this.api.i18n.register(Translations);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.config = ModuleConfig.parse(this.api.config.get(CONFIG_KEY));
|
this.config = ModuleConfig.parse(rawConfig);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to init module", e);
|
console.error("Failed to init module", e);
|
||||||
throw new Error(`Errors in module configuration for "${ModuleName}"`);
|
throw new Error(`Errors in module configuration for "${ModuleName}"`);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineProject } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineProject({
|
||||||
|
test: {
|
||||||
|
exclude: ["./e2e/**/*", "./node_modules/**/*"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -236,13 +236,29 @@ for (const auth of ["mas", "legacy"] as const) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe(`Restricted guests auth=${auth} guests=${guestsEnabled}`, () => {
|
test.describe(`Restricted guests auth=${auth} guests=${guestsEnabled}`, () => {
|
||||||
test("should error if config is missing", async ({ page }) => {
|
test("should not error if config is missing", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
|
await expect(page.getByText("Your Element is misconfigured")).not.toBeVisible();
|
||||||
await expect(page.getByText("Errors in module configuration")).toBeVisible();
|
await expect(page.getByText("Errors in module configuration")).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("with config", () => {
|
test.describe("with invalid config", () => {
|
||||||
|
test.use({
|
||||||
|
config: {
|
||||||
|
"io.element.element-web-modules.restricted-guests": {
|
||||||
|
hs_url: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render error", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
|
||||||
|
await expect(page.getByText("Errors in module configuration")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("with valid config", () => {
|
||||||
test.beforeEach(async ({ config, guestHomeserver, page, testRoomId }) => {
|
test.beforeEach(async ({ config, guestHomeserver, page, testRoomId }) => {
|
||||||
config["io.element.element-web-modules.restricted-guests"] = {
|
config["io.element.element-web-modules.restricted-guests"] = {
|
||||||
guest_user_homeserver_url: guestHomeserver.baseUrl,
|
guest_user_homeserver_url: guestHomeserver.baseUrl,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const CONFIG_KEY = "io.element.element-web-modules.restricted-guests";
|
|||||||
|
|
||||||
declare module "@element-hq/element-web-module-api" {
|
declare module "@element-hq/element-web-module-api" {
|
||||||
export interface Config {
|
export interface Config {
|
||||||
[CONFIG_KEY]: input<ConfigSchema>;
|
[CONFIG_KEY]?: input<ConfigSchema>;
|
||||||
sso_redirect_options?: {
|
sso_redirect_options?: {
|
||||||
immediate?: boolean; // incompatible option
|
immediate?: boolean; // incompatible option
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { type Api } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
|
import RestrictedGuestsModule from "./index";
|
||||||
|
|
||||||
|
const makeApi = (config: unknown): Api => {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
get: vi.fn().mockReturnValue(config),
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
register: vi.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Api;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("RestrictedGuestsModule", () => {
|
||||||
|
describe("load", () => {
|
||||||
|
it("should do nothing if no config present", async () => {
|
||||||
|
const api = makeApi(null);
|
||||||
|
|
||||||
|
const module = new RestrictedGuestsModule(api);
|
||||||
|
await module.load();
|
||||||
|
|
||||||
|
expect(api.i18n.register).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,12 +31,18 @@ class RestrictedGuestsModule implements Module {
|
|||||||
public constructor(private api: Api) {}
|
public constructor(private api: Api) {}
|
||||||
|
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
|
const rawConfig = this.api.config.get(CONFIG_KEY);
|
||||||
|
if (!rawConfig) {
|
||||||
|
console.debug(`No configuration found for module "${ModuleName}", skipping initialization.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.adoptedStyleSheets.push(compound);
|
document.adoptedStyleSheets.push(compound);
|
||||||
|
|
||||||
this.api.i18n.register(Translations);
|
this.api.i18n.register(Translations);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.config = ModuleConfig.parse(this.api.config.get(CONFIG_KEY));
|
this.config = ModuleConfig.parse(rawConfig);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to init module", e);
|
console.error("Failed to init module", e);
|
||||||
throw new Error(`Errors in module configuration for "${ModuleName}"`);
|
throw new Error(`Errors in module configuration for "${ModuleName}"`);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineProject } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineProject({
|
||||||
|
test: {
|
||||||
|
exclude: ["./e2e/**/*", "./node_modules/**/*"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { vi, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import WidgetLifecycleModule, { type WidgetLifecycleApiAdapter } from "./WidgetLifecycleModule";
|
import WidgetLifecycleModule, { type WidgetLifecycleApiAdapter } from "./WidgetLifecycleModule";
|
||||||
import type {
|
import type {
|
||||||
@@ -24,15 +24,15 @@ const createApi = (config: unknown = {}) => {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const widgetLifecycle: WidgetLifecycleApiAdapter = {
|
const widgetLifecycle: WidgetLifecycleApiAdapter = {
|
||||||
registerPreloadApprover: (approver) => {
|
registerPreloadApprover: vi.fn((approver) => {
|
||||||
handlers.preload = approver;
|
handlers.preload = approver;
|
||||||
},
|
}),
|
||||||
registerIdentityApprover: (approver) => {
|
registerIdentityApprover: vi.fn((approver) => {
|
||||||
handlers.identity = approver;
|
handlers.identity = approver;
|
||||||
},
|
}),
|
||||||
registerCapabilitiesApprover: (approver) => {
|
registerCapabilitiesApprover: vi.fn((approver) => {
|
||||||
handlers.capabilities = approver;
|
handlers.capabilities = approver;
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -55,6 +55,17 @@ const widget: WidgetDescriptor = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("WidgetLifecycleModule", () => {
|
describe("WidgetLifecycleModule", () => {
|
||||||
|
it("does nothing when no config is present", async () => {
|
||||||
|
const { api } = createApi(null);
|
||||||
|
|
||||||
|
const module = new WidgetLifecycleModule(api);
|
||||||
|
await module.load();
|
||||||
|
|
||||||
|
expect(api.widgetLifecycle.registerPreloadApprover).not.toHaveBeenCalled();
|
||||||
|
expect(api.widgetLifecycle.registerIdentityApprover).not.toHaveBeenCalled();
|
||||||
|
expect(api.widgetLifecycle.registerCapabilitiesApprover).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("approves preload when configured", async () => {
|
it("approves preload when configured", async () => {
|
||||||
const { api, handlers } = createApi({
|
const { api, handlers } = createApi({
|
||||||
widget_permissions: {
|
widget_permissions: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { Api, Module, WidgetDescriptor, WidgetLifecycleApi } from "@element
|
|||||||
import { CONFIG_KEY, parseWidgetLifecycleConfig, type WidgetLifecycleModuleConfig } from "./config";
|
import { CONFIG_KEY, parseWidgetLifecycleConfig, type WidgetLifecycleModuleConfig } from "./config";
|
||||||
import { constructWidgetPermissions } from "./utils/constructWidgetPermissions";
|
import { constructWidgetPermissions } from "./utils/constructWidgetPermissions";
|
||||||
import { matchPattern } from "./utils/matchPattern";
|
import { matchPattern } from "./utils/matchPattern";
|
||||||
|
import { name as ModuleName } from "../package.json";
|
||||||
|
|
||||||
/** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */
|
/** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */
|
||||||
export type WidgetLifecycleApiAdapter = Pick<
|
export type WidgetLifecycleApiAdapter = Pick<
|
||||||
@@ -30,6 +31,12 @@ export default class WidgetLifecycleModule implements Module {
|
|||||||
public constructor(private api: ModuleApi) {}
|
public constructor(private api: ModuleApi) {}
|
||||||
|
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
|
const rawConfig = this.api.config.get(CONFIG_KEY);
|
||||||
|
if (!rawConfig) {
|
||||||
|
console.debug(`No configuration found for module "${ModuleName}", skipping initialization.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.api.widgetLifecycle) {
|
if (!this.api.widgetLifecycle) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Widget lifecycle API is not available. Update Element Web to a build that provides widget lifecycle module support.",
|
"Widget lifecycle API is not available. Update Element Web to a build that provides widget lifecycle module support.",
|
||||||
@@ -37,7 +44,7 @@ export default class WidgetLifecycleModule implements Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.config = parseWidgetLifecycleConfig(this.api.config.get(CONFIG_KEY));
|
this.config = parseWidgetLifecycleConfig(rawConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[WidgetLifecycle] Failed to init module", error);
|
console.error("[WidgetLifecycle] Failed to init module", error);
|
||||||
this.config = {};
|
this.config = {};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export type WidgetLifecycleModuleConfig = Record<string, WidgetConfiguration>;
|
|||||||
|
|
||||||
declare module "@element-hq/element-web-module-api" {
|
declare module "@element-hq/element-web-module-api" {
|
||||||
export interface Config {
|
export interface Config {
|
||||||
[CONFIG_KEY]: z.input<typeof ModuleConfigSchema>;
|
[CONFIG_KEY]?: z.input<typeof ModuleConfigSchema>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,12 +90,28 @@ test.describe("widget-toggles", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should error if config is missing", async ({ page }) => {
|
test("should not error if config is missing", async ({ page }) => {
|
||||||
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
|
await expect(page.getByText("Your Element is misconfigured")).not.toBeVisible();
|
||||||
await expect(page.getByText("Errors in module configuration")).toBeVisible();
|
await expect(page.getByText("Errors in module configuration")).not.toBeVisible();
|
||||||
// We don't take a screenshot as we don't want to assert Element's styling, only our own
|
// We don't take a screenshot as we don't want to assert Element's styling, only our own
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("with invalid config", () => {
|
||||||
|
test.use({
|
||||||
|
config: {
|
||||||
|
"io.element.element-web-modules.widget-toggles": {
|
||||||
|
foo: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render error", async ({ page }) => {
|
||||||
|
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
|
||||||
|
await expect(page.getByText("Errors in module configuration")).toBeVisible();
|
||||||
|
// We don't take a screenshot as we don't want to assert Element's styling, only our own
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("with correct config", () => {
|
test.describe("with correct config", () => {
|
||||||
test.use({
|
test.use({
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ export const CONFIG_KEY = "io.element.element-web-modules.widget-toggles";
|
|||||||
|
|
||||||
declare module "@element-hq/element-web-module-api" {
|
declare module "@element-hq/element-web-module-api" {
|
||||||
export interface Config {
|
export interface Config {
|
||||||
[CONFIG_KEY]: input<WidgetTogglesConfig>;
|
[CONFIG_KEY]?: input<WidgetTogglesConfig>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
|
|||||||
import type { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api";
|
import type { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api";
|
||||||
import { CONFIG_KEY, WidgetTogglesConfig } from "./config";
|
import { CONFIG_KEY, WidgetTogglesConfig } from "./config";
|
||||||
import { WidgetToggle } from "./toggle";
|
import { WidgetToggle } from "./toggle";
|
||||||
|
import { name as ModuleName } from "../package.json";
|
||||||
|
|
||||||
class WidgetToggleModule implements Module {
|
class WidgetToggleModule implements Module {
|
||||||
public static readonly moduleApiVersion = "^1.12.0";
|
public static readonly moduleApiVersion = "^1.12.0";
|
||||||
@@ -19,8 +20,14 @@ class WidgetToggleModule implements Module {
|
|||||||
public constructor(private api: Api) {}
|
public constructor(private api: Api) {}
|
||||||
|
|
||||||
public async load(): Promise<void> {
|
public async load(): Promise<void> {
|
||||||
|
const rawConfig = this.api.config.get(CONFIG_KEY);
|
||||||
|
if (!rawConfig) {
|
||||||
|
console.debug(`No configuration found for module "${ModuleName}", skipping initialization.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.config = WidgetTogglesConfig.parse(this.api.config.get(CONFIG_KEY));
|
this.config = WidgetTogglesConfig.parse(rawConfig);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to init module", e);
|
console.error("Failed to init module", e);
|
||||||
throw new Error(`Errors in module configuration for widget toggles module`);
|
throw new Error(`Errors in module configuration for widget toggles module`);
|
||||||
|
|||||||
Reference in New Issue
Block a user