merge: upstream Element Call v0.28.0 (cinny #201) #61
@@ -8,8 +8,8 @@ Please see LICENSE in the repository root for full details.
|
||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { validateConfig } from "./Config";
|
||||
import { MatrixRTCMode } from "./ConfigOptions";
|
||||
import { Config, validateConfig } from "./Config";
|
||||
import { DEFAULT_CONFIG, MatrixRTCMode } from "./ConfigOptions";
|
||||
|
||||
describe("validateConfig", () => {
|
||||
afterEach(() => {
|
||||
@@ -52,3 +52,52 @@ describe("validateConfig", () => {
|
||||
expect(result.ssla).toBe("https://example.invalid/ssla");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Config.initWith", () => {
|
||||
// vitest.setup.ts has already called initDefault(), so every test here is
|
||||
// free to re-initialize; the last call wins.
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
Config.initDefault();
|
||||
});
|
||||
|
||||
it("makes the supplied config readable", () => {
|
||||
Config.initWith({ ssla: "https://example.invalid/ssla" });
|
||||
expect(Config.get().ssla).toBe("https://example.invalid/ssla");
|
||||
});
|
||||
|
||||
it("fills in defaults for keys the embedder did not supply", () => {
|
||||
Config.initWith({ ssla: "https://example.invalid/ssla" });
|
||||
expect(Config.get().media_quality).toEqual(DEFAULT_CONFIG.media_quality);
|
||||
});
|
||||
|
||||
it("validates the supplied config just as a fetched one would be", () => {
|
||||
const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
|
||||
Config.initWith({
|
||||
matrix_rtc_mode: "nonsense" as unknown as MatrixRTCMode,
|
||||
});
|
||||
expect(Config.get().matrix_rtc_mode).toBeUndefined();
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not share nested state with DEFAULT_CONFIG", () => {
|
||||
Config.initWith({});
|
||||
expect(Config.get().media_quality).not.toBe(DEFAULT_CONFIG.media_quality);
|
||||
});
|
||||
|
||||
it("stops a later init() from fetching over the top of it", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
||||
Config.initWith({ ssla: "https://example.invalid/ssla" });
|
||||
|
||||
await Config.init();
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
expect(Config.get().ssla).toBe("https://example.invalid/ssla");
|
||||
});
|
||||
|
||||
it("replaces a config initialized earlier", () => {
|
||||
Config.initWith({ ssla: "https://first.invalid/ssla" });
|
||||
Config.initWith({ ssla: "https://second.invalid/ssla" });
|
||||
expect(Config.get().ssla).toBe("https://second.invalid/ssla");
|
||||
});
|
||||
});
|
||||
|
||||
+42
-7
@@ -30,6 +30,14 @@ export class Config {
|
||||
return this.internalInstance.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the config by fetching `config.json`, locating it relative to
|
||||
* the current page.
|
||||
*
|
||||
* Does nothing if the config has already been initialized, including by
|
||||
* {@link Config.initWith}, so that the regular startup path can run unchanged
|
||||
* when an embedder has already supplied the config.
|
||||
*/
|
||||
public static async init(): Promise<void> {
|
||||
if (!Config.internalInstance?.initPromise) {
|
||||
const internalInstance = new Config();
|
||||
@@ -50,17 +58,36 @@ export class Config {
|
||||
|
||||
Config.internalInstance.initPromise = downloadConfig(fetchTarget).then(
|
||||
(config) => {
|
||||
internalInstance.config = merge(
|
||||
{},
|
||||
DEFAULT_CONFIG,
|
||||
validateConfig(config),
|
||||
);
|
||||
internalInstance.config = resolveConfig(config);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Config.internalInstance.initPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the config from an object supplied by the embedder, instead of
|
||||
* fetching `config.json`.
|
||||
*
|
||||
* {@link Config.init} derives the location of `config.json` from
|
||||
* `window.location`, which only makes sense while Element Call owns the page.
|
||||
* When it is embedded in a host application the host owns the configuration
|
||||
* and passes it in here.
|
||||
*
|
||||
* The config goes through the same validation and defaulting as a fetched
|
||||
* one, so that an injected config behaves identically to a hosted one.
|
||||
*
|
||||
* Replaces any config initialized earlier.
|
||||
*/
|
||||
public static initWith(config: ConfigOptions): void {
|
||||
const internalInstance = new Config();
|
||||
internalInstance.config = resolveConfig(config);
|
||||
// Mark initialization as already done, so that a later init() resolves
|
||||
// immediately rather than fetching config.json over the top of this.
|
||||
internalInstance.initPromise = Promise.resolve();
|
||||
Config.internalInstance = internalInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a alternative initializer that does not load anything
|
||||
* from a hosted config file but instead just initializes the config using the
|
||||
@@ -69,8 +96,7 @@ export class Config {
|
||||
* It is supposed to only be used in tests. (It is executed in `vite.setup.js`)
|
||||
*/
|
||||
public static initDefault(): void {
|
||||
Config.internalInstance = new Config();
|
||||
Config.internalInstance.config = { ...DEFAULT_CONFIG };
|
||||
Config.initWith({});
|
||||
}
|
||||
|
||||
// Convenience accessors
|
||||
@@ -94,6 +120,15 @@ export class Config {
|
||||
private initPromise?: Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies validation and the built-in defaults to a config, however it was
|
||||
* obtained. Deep-merges onto a fresh object so that the result never shares
|
||||
* nested state with {@link DEFAULT_CONFIG}.
|
||||
*/
|
||||
function resolveConfig(config: ConfigOptions): ResolvedConfigOptions {
|
||||
return merge({}, DEFAULT_CONFIG, validateConfig(config));
|
||||
}
|
||||
|
||||
export function validateConfig(config: ConfigOptions): ConfigOptions {
|
||||
const mode = config.matrix_rtc_mode;
|
||||
if (mode !== undefined && !VALID_MATRIX_RTC_MODES.has(mode)) {
|
||||
|
||||
Reference in New Issue
Block a user