mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-05 15:58:19 +00:00
Expose NFC path identity across supported transports
This commit is contained in:
1 parent
0e458e5e20
commit
ca4e6d38b9
22 files changed
+1003
-268
No files matched your search
@@ -1,5 +1,4 @@
|
||||
import { ChunkAlgorithmNames } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { $msg } from "@/common/translation";
|
||||
import type { SettingSpecGroup } from "./SettingSpec.ts";
|
||||
|
||||
export type AdvancedSettingSpecContext = {
|
||||
@@ -24,18 +23,6 @@ export function createAdvancedSettingSpecGroups({
|
||||
},
|
||||
{ key: "customChunkSize", control: { type: "number", min: 0, allowZero: true } },
|
||||
{ key: "strictContentCheckOnStartup", control: { type: "toggle" } },
|
||||
{
|
||||
key: "pathNormalizationVersion",
|
||||
holdValue: true,
|
||||
control: {
|
||||
type: "numeric-dropdown",
|
||||
options: () => ({
|
||||
"0": $msg("Legacy path identity"),
|
||||
"1": $msg("Unicode NFC path identity"),
|
||||
}),
|
||||
},
|
||||
visible: isCouchDB,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ export type GeneralSettingSpecContext = {
|
||||
showVerboseLog: () => boolean;
|
||||
};
|
||||
|
||||
/** Build the shared Appearance and Logging controls. */
|
||||
/** Build the shared General Settings controls. */
|
||||
export function createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails,
|
||||
showVerboseLog,
|
||||
@@ -61,6 +61,22 @@ export function createGeneralSettingSpecGroups({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "File identity",
|
||||
items: [
|
||||
{
|
||||
key: "pathNormalizationVersion",
|
||||
holdValue: true,
|
||||
control: {
|
||||
type: "numeric-dropdown",
|
||||
options: () => ({
|
||||
"0": $msg("Legacy path identity"),
|
||||
"1": $msg("Unicode NFC path identity"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+15
-3
@@ -319,7 +319,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
).toEqual(["Copy the current settings to a Setup URI", "Show QR code"]);
|
||||
});
|
||||
|
||||
it("keeps Appearance, Logging, and Extra menus inside General Settings", () => {
|
||||
it("keeps Appearance, Logging, File identity, and Extra menus inside General Settings", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const general = definitions.find(
|
||||
@@ -332,7 +332,19 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
(item): item is SettingDefinitionPage => isPage(item) && item.name === "🎚️ Extra menus"
|
||||
);
|
||||
|
||||
expect(generalPages?.map(({ name }) => name)).toEqual(["🎨 Appearance", "📝 Logging", "🎚️ Extra menus"]);
|
||||
expect(generalPages?.map(({ name }) => name)).toEqual([
|
||||
"🎨 Appearance",
|
||||
"📝 Logging",
|
||||
"🗂️ File identity",
|
||||
"🎚️ Extra menus",
|
||||
]);
|
||||
const fileIdentity = generalPages?.find(({ name }) => name === "🗂️ File identity");
|
||||
expect(
|
||||
fileIdentity?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual(["pathNormalizationVersion"]);
|
||||
expect(
|
||||
fileIdentity?.items?.filter((item) => "action" in item && typeof item.action === "function")
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
appearance?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual([
|
||||
@@ -390,7 +402,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
const tab = createSettingsTab();
|
||||
const pages = collectPages(tab.getSettingDefinitions());
|
||||
|
||||
expect(pages).toHaveLength(14);
|
||||
expect(pages).toHaveLength(15);
|
||||
expect(pages.map(({ name }) => name)).toEqual(
|
||||
expect.arrayContaining(
|
||||
createSettingsPageCatalogue()
|
||||
|
||||
@@ -892,9 +892,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
});
|
||||
const [appearance, logging] = groups;
|
||||
if (!appearance || !logging) {
|
||||
throw new Error("General settings must define Appearance and Logging groups");
|
||||
const [appearance, logging, fileIdentity] = groups;
|
||||
if (!appearance || !logging || !fileIdentity) {
|
||||
throw new Error("General settings must define Appearance, Logging, and File identity groups");
|
||||
}
|
||||
return this.createRootGroup("general-settings", [
|
||||
{
|
||||
@@ -907,6 +907,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
name: `📝 ${logging.heading}`,
|
||||
items: logging.items,
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
name: `🗂️ ${fileIdentity.heading}`,
|
||||
items: [this.createRebuildRequiredAction(), ...(fileIdentity.items ?? [])],
|
||||
},
|
||||
this.createExtraMenusPage(),
|
||||
]);
|
||||
}
|
||||
@@ -1128,7 +1133,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
if (method === "remoteOnly" && this.isDirty("pathNormalizationVersion")) {
|
||||
Logger(
|
||||
$msg(
|
||||
"CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1."
|
||||
"Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild from a complete authoritative Vault and Fetch on the other devices. With P2P, Rebuild prepares only the source device's local database. Resolve colliding file names first."
|
||||
),
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
|
||||
@@ -581,11 +581,11 @@ describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
|
||||
expect(Reflect.get(tab.editingSettings, "pathNormalizationVersion")).toBe(1);
|
||||
expect(tab.isDirty("pathNormalizationVersion")).toBe(true);
|
||||
expect(vi.mocked($msg)).toHaveBeenCalledWith(
|
||||
"CouchDB only. Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild both databases from a complete authoritative Vault and Fetch on the other devices. Resolve colliding file names first. P2P and Object Storage do not support version 1."
|
||||
"Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild from a complete authoritative Vault and Fetch on the other devices. With P2P, Rebuild prepares only the source device's local database. Resolve colliding file names first."
|
||||
);
|
||||
expect(loggerMocks.Logger).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"Rebuild both databases from a complete authoritative Vault and Fetch on the other devices."
|
||||
"Rebuild from a complete authoritative Vault and Fetch on the other devices."
|
||||
),
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
@@ -692,6 +692,30 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
return { tab, saveSettingData };
|
||||
}
|
||||
|
||||
it("includes path identity once in the declarative General settings group", () => {
|
||||
const { tab } = createSettingsTab();
|
||||
const group = (
|
||||
tab as unknown as {
|
||||
createGeneralSettingsGroup: () => {
|
||||
items: Array<{
|
||||
name: string;
|
||||
items?: Array<{ control?: { key: string }; action?: () => void; visible?: () => boolean }>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
).createGeneralSettingsGroup();
|
||||
const fileIdentity = group.items.find(({ name }) => name === "🗂️ File identity");
|
||||
|
||||
expect(fileIdentity).toBeDefined();
|
||||
const apply = fileIdentity?.items?.find(({ action }) => typeof action === "function");
|
||||
expect(apply).toBeDefined();
|
||||
Reflect.set(tab.editingSettings, "pathNormalizationVersion", 1);
|
||||
expect(apply?.visible?.()).toBe(true);
|
||||
expect(fileIdentity?.items?.flatMap(({ control }) => (control ? [control.key] : []))).toEqual([
|
||||
"pathNormalizationVersion",
|
||||
]);
|
||||
});
|
||||
|
||||
it("awaits the stored passphrase before initialising the editing buffer", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
tab.resetEditingSettings();
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("paneAdvanced", () => {
|
||||
};
|
||||
|
||||
paneAdvanced.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(11));
|
||||
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(10));
|
||||
|
||||
expect(addPanel.mock.calls.map(([, heading]) => heading)).toEqual([
|
||||
"Memory cache",
|
||||
@@ -56,7 +56,6 @@ describe("paneAdvanced", () => {
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
@@ -69,7 +68,6 @@ describe("paneAdvanced", () => {
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"strictContentCheckOnStartup",
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { paneGeneral } from "./PaneGeneral.ts";
|
||||
|
||||
const settingHarness = vi.hoisted(() => ({
|
||||
rendered: [] as unknown[],
|
||||
}));
|
||||
|
||||
vi.mock("@/common/translation", () => ({
|
||||
$msg: (key: string) => key,
|
||||
$t: (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
LiveSyncSetting: class LiveSyncSetting {},
|
||||
}));
|
||||
|
||||
vi.mock("./SettingSpec.ts", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("./SettingSpec.ts")>();
|
||||
return {
|
||||
...original,
|
||||
renderLegacySettingSpec: vi.fn((_renderer: unknown, spec: unknown) => {
|
||||
settingHarness.rendered.push(spec);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
settingHarness.rendered.length = 0;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("paneGeneral", () => {
|
||||
it("renders path identity once in the File identity panel and preserves its held value", async () => {
|
||||
const addPanel = vi.fn((_parent: HTMLElement, heading: string) =>
|
||||
Promise.resolve({ heading } as unknown as HTMLElement)
|
||||
);
|
||||
const host = {
|
||||
isConfiguredAs: vi.fn(() => false),
|
||||
};
|
||||
|
||||
paneGeneral.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(14));
|
||||
|
||||
expect(addPanel.mock.calls.map(([, heading]) => heading)).toEqual([
|
||||
"obsidianLiveSyncSettingTab.titleAppearance",
|
||||
"obsidianLiveSyncSettingTab.titleLogging",
|
||||
"File identity",
|
||||
"obsidianLiveSyncSettingTab.titleExtraMenus",
|
||||
]);
|
||||
const pathIdentity = settingHarness.rendered.filter(
|
||||
(spec) => (spec as { key: string }).key === "pathNormalizationVersion"
|
||||
);
|
||||
expect(pathIdentity).toHaveLength(1);
|
||||
expect(pathIdentity[0]).toMatchObject({ holdValue: true });
|
||||
expect((pathIdentity[0] as { visible?: () => boolean }).visible).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const rangeMessages = {
|
||||
};
|
||||
|
||||
describe("Advanced setting specifications", () => {
|
||||
it("lists only the eleven controls currently exposed by the Advanced page", () => {
|
||||
it("lists only the ten controls currently exposed by the Advanced page", () => {
|
||||
const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => true });
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual([
|
||||
@@ -31,7 +31,6 @@ describe("Advanced setting specifications", () => {
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"strictContentCheckOnStartup",
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
@@ -52,13 +51,12 @@ describe("Advanced setting specifications", () => {
|
||||
expect(spec.disabled).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps path identity and the four remote chunk controls CouchDB-only", () => {
|
||||
it("keeps the four remote chunk controls CouchDB-only", () => {
|
||||
let couchDB = false;
|
||||
const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => couchDB });
|
||||
const conditional = groups.flatMap(({ items }) => items).filter(({ visible }) => visible !== undefined);
|
||||
|
||||
expect(conditional.map(({ key }) => key)).toEqual([
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
@@ -70,18 +68,67 @@ describe("Advanced setting specifications", () => {
|
||||
expect(conditional.every(({ visible }) => visible?.() === true)).toBe(true);
|
||||
});
|
||||
|
||||
it("exposes CouchDB path identity as a two-choice numeric dropdown", () => {
|
||||
let couchDB = false;
|
||||
const spec = createAdvancedSettingSpecGroups({ isCouchDB: () => couchDB })
|
||||
it("excludes transient settings-dialogue keys from the persisted key type", () => {
|
||||
type OnDialogOverlap = Extract<keyof OnDialogSettings, PersistedSettingKey>;
|
||||
expectTypeOf<OnDialogOverlap>().toEqualTypeOf<never>();
|
||||
});
|
||||
|
||||
it("resolves translated Commonlib metadata for every explicitly exposed key", () => {
|
||||
const specs = [
|
||||
...createAdvancedSettingSpecGroups({ isCouchDB: () => true }),
|
||||
...createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => true,
|
||||
showVerboseLog: () => true,
|
||||
}),
|
||||
].flatMap(({ items }) => items);
|
||||
|
||||
for (const spec of specs) {
|
||||
const metadata = getConfig(spec.key);
|
||||
expect(metadata, spec.key).not.toBe(false);
|
||||
if (!metadata) {
|
||||
throw new Error(`Missing setting metadata for ${spec.key}`);
|
||||
}
|
||||
const definition = toObsidianSettingDefinition(spec, metadata, rangeMessages);
|
||||
expect(definition.name).toBe(`${metadata.name}${statusDisplay(metadata.status)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("General setting specifications", () => {
|
||||
it("shares every General, Logging, and File identity control between the imperative and declarative renderers", () => {
|
||||
const groups = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => true,
|
||||
showVerboseLog: () => true,
|
||||
});
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual(["Appearance", "Logging", "File identity"]);
|
||||
expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([
|
||||
"displayLanguage",
|
||||
"showStatusOnEditor",
|
||||
"showOnlyIconsOnEditor",
|
||||
"showStatusOnStatusbar",
|
||||
"hideFileWarningNotice",
|
||||
"syncGuardEnabled",
|
||||
"syncGuardMinimumFiles",
|
||||
"networkWarningStyle",
|
||||
"lessInformationInLog",
|
||||
"showVerboseLog",
|
||||
"pathNormalizationVersion",
|
||||
]);
|
||||
});
|
||||
|
||||
it("exposes path identity for every remote type as a reconstruction-required dropdown", () => {
|
||||
const spec = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => false,
|
||||
showVerboseLog: () => false,
|
||||
})
|
||||
.flatMap(({ items }) => items)
|
||||
.find(({ key }) => key === "pathNormalizationVersion");
|
||||
if (!spec) throw new Error("Path identity mode is not exposed on the Advanced page");
|
||||
if (!spec) throw new Error("Path identity mode is not exposed in General settings");
|
||||
if (spec.control.type !== "numeric-dropdown") throw new Error("Expected a numeric dropdown");
|
||||
|
||||
expect(spec.control).toMatchObject({ type: "numeric-dropdown", options: expect.any(Function) });
|
||||
expect(spec.visible?.()).toBe(false);
|
||||
couchDB = true;
|
||||
expect(spec.visible?.()).toBe(true);
|
||||
expect(spec).toMatchObject({ key: "pathNormalizationVersion", holdValue: true });
|
||||
expect(spec.visible).toBeUndefined();
|
||||
expect(spec.control.options()).toEqual({
|
||||
"0": "Legacy path identity",
|
||||
"1": "Unicode NFC path identity",
|
||||
@@ -102,54 +149,11 @@ describe("Advanced setting specifications", () => {
|
||||
key: "pathNormalizationVersion",
|
||||
options: {
|
||||
holdValue: true,
|
||||
onUpdate: expect.any(Function),
|
||||
options: { "0": "Legacy path identity", "1": "Unicode NFC path identity" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("excludes transient settings-dialogue keys from the persisted key type", () => {
|
||||
type OnDialogOverlap = Extract<keyof OnDialogSettings, PersistedSettingKey>;
|
||||
expectTypeOf<OnDialogOverlap>().toEqualTypeOf<never>();
|
||||
});
|
||||
|
||||
it("resolves translated Commonlib metadata for every explicitly exposed key", () => {
|
||||
const specs = createAdvancedSettingSpecGroups({ isCouchDB: () => true }).flatMap(({ items }) => items);
|
||||
|
||||
for (const spec of specs) {
|
||||
const metadata = getConfig(spec.key);
|
||||
expect(metadata, spec.key).not.toBe(false);
|
||||
if (!metadata) {
|
||||
throw new Error(`Missing setting metadata for ${spec.key}`);
|
||||
}
|
||||
const definition = toObsidianSettingDefinition(spec, metadata, rangeMessages);
|
||||
expect(definition.name).toBe(`${metadata.name}${statusDisplay(metadata.status)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("General setting specifications", () => {
|
||||
it("shares every General and Logging control between the imperative and declarative renderers", () => {
|
||||
const groups = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => true,
|
||||
showVerboseLog: () => true,
|
||||
});
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual(["Appearance", "Logging"]);
|
||||
expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([
|
||||
"displayLanguage",
|
||||
"showStatusOnEditor",
|
||||
"showOnlyIconsOnEditor",
|
||||
"showStatusOnStatusbar",
|
||||
"hideFileWarningNotice",
|
||||
"syncGuardEnabled",
|
||||
"syncGuardMinimumFiles",
|
||||
"networkWarningStyle",
|
||||
"lessInformationInLog",
|
||||
"showVerboseLog",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the three feature-level controls together under Extra menus", () => {
|
||||
const group = createExtraMenuSettingSpecGroup();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/common/translation", () => ({
|
||||
$msg: (key: string) => key,
|
||||
$t: (key: string) => key,
|
||||
translateLiveSyncMessage: (key: string) => key,
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||
@@ -20,6 +21,7 @@ vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||
|
||||
import {
|
||||
createAdvancedSettingDefinitionGroups,
|
||||
createGeneralSettingDefinitionGroups,
|
||||
createSettingsPageCatalogue,
|
||||
getSettingsRootGroupEntry,
|
||||
type SettingsRootGroupId,
|
||||
@@ -87,7 +89,6 @@ describe("settings page catalogue", () => {
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"strictContentCheckOnStartup",
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
"concurrencyOfReadChunksOnline",
|
||||
@@ -97,4 +98,22 @@ describe("settings page catalogue", () => {
|
||||
]);
|
||||
expect(new Set(keys).size).toBe(keys.length);
|
||||
});
|
||||
|
||||
it("registers path identity once in General settings and leaves it out of Advanced", () => {
|
||||
const generalGroups = createGeneralSettingDefinitionGroups({
|
||||
showEditorStatusDetails: () => true,
|
||||
showVerboseLog: () => true,
|
||||
});
|
||||
const generalKeys = generalGroups.flatMap(({ items = [] }) =>
|
||||
items.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
);
|
||||
const advancedKeys = createAdvancedSettingDefinitionGroups({ isCouchDB: () => true }).flatMap(
|
||||
({ items = [] }) =>
|
||||
items.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
);
|
||||
|
||||
expect(generalGroups.map(({ heading }) => heading)).toContain("File identity");
|
||||
expect(generalKeys.filter((key) => key === "pathNormalizationVersion")).toHaveLength(1);
|
||||
expect(advancedKeys).not.toContain("pathNormalizationVersion");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user