test: cover element schema migration flow
This commit is contained in:
@@ -62,7 +62,6 @@ describe("parseClipboard()", () => {
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
}),
|
||||
]);
|
||||
expect(clipboardData.schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
||||
});
|
||||
|
||||
it("should parse valid excalidraw JSON if inside text/html", async () => {
|
||||
@@ -109,12 +108,11 @@ describe("parseClipboard()", () => {
|
||||
// -------------------------------------------------------------------------
|
||||
});
|
||||
|
||||
it("should preserve per-element schema when payload schema is missing", async () => {
|
||||
it("should preserve per-element schema on clipboard payload", async () => {
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
const clipboardPayload = JSON.parse(
|
||||
serializeAsClipboardJSON({ elements: [rect], files: null }),
|
||||
);
|
||||
delete clipboardPayload.schemaVersion;
|
||||
|
||||
const clipboardData = await parseClipboard(
|
||||
await parseDataTransferEvent(
|
||||
@@ -126,7 +124,6 @@ describe("parseClipboard()", () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(clipboardData.schemaVersion).toBeUndefined();
|
||||
expect(clipboardData.elements?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: rect.id,
|
||||
@@ -135,6 +132,32 @@ describe("parseClipboard()", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should not upcast legacy elements to latest schema on clipboard serialize", async () => {
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
const legacyRect = { ...rect } as typeof rect & { schemaVersion?: number };
|
||||
delete legacyRect.schemaVersion;
|
||||
|
||||
const clipboardPayload = JSON.parse(
|
||||
serializeAsClipboardJSON({
|
||||
elements: [legacyRect as typeof rect],
|
||||
files: null,
|
||||
}),
|
||||
);
|
||||
expect(clipboardPayload.elements[0]).not.toHaveProperty("schemaVersion");
|
||||
|
||||
const clipboardData = await parseClipboard(
|
||||
await parseDataTransferEvent(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": JSON.stringify(clipboardPayload),
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(clipboardData.elements?.[0]).not.toHaveProperty("schemaVersion");
|
||||
});
|
||||
|
||||
it("should parse <image> `src` urls out of text/html", async () => {
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -3,13 +3,8 @@ import { DEFAULT_ELEMENT_PROPS } from "@excalidraw/common";
|
||||
import { API } from "../tests/helpers/api";
|
||||
|
||||
import {
|
||||
ALL_SCOPES,
|
||||
hasElementSchemaVersion,
|
||||
type SchemaMigration,
|
||||
migrateAPIElements,
|
||||
migrateClipboardElements,
|
||||
migrateLibraryElements,
|
||||
migrateSceneElements,
|
||||
migrateElements,
|
||||
resolveSchemaVersion,
|
||||
SCHEMA_MIGRATIONS,
|
||||
SCHEMA_VERSIONS,
|
||||
@@ -18,16 +13,20 @@ import {
|
||||
|
||||
describe("schema migration", () => {
|
||||
it("should migrate legacy frame backgrounds to transparent", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ffc9c9",
|
||||
});
|
||||
const frame = {
|
||||
...API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ffc9c9",
|
||||
}),
|
||||
schemaVersion: undefined,
|
||||
};
|
||||
|
||||
const migrated = migrateSceneElements([frame], SCHEMA_VERSIONS.initial)!;
|
||||
const migrated = migrateElements([frame])!;
|
||||
|
||||
expect(migrated[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
);
|
||||
expect(migrated[0].schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
||||
});
|
||||
|
||||
it("should keep latest-schema frame backgrounds unchanged", () => {
|
||||
@@ -35,44 +34,36 @@ describe("schema migration", () => {
|
||||
type: "frame",
|
||||
backgroundColor: "#ffc9c9",
|
||||
});
|
||||
const latestFrame = {
|
||||
...frame,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof frame & { schemaVersion: number };
|
||||
|
||||
const migrated = migrateSceneElements([frame], SCHEMA_VERSIONS.latest)!;
|
||||
const migrated = migrateElements([latestFrame])!;
|
||||
|
||||
expect(migrated[0].backgroundColor).toBe("#ffc9c9");
|
||||
expect(migrated[0].schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
||||
});
|
||||
|
||||
it("should normalize legacy frame backgrounds across all scopes", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#a5d8ff",
|
||||
});
|
||||
it("should normalize legacy frame backgrounds", () => {
|
||||
const frame = {
|
||||
...API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#a5d8ff",
|
||||
}),
|
||||
schemaVersion: undefined,
|
||||
};
|
||||
|
||||
const migrationsByScope = {
|
||||
scene: migrateSceneElements,
|
||||
library: migrateLibraryElements,
|
||||
clipboard: migrateClipboardElements,
|
||||
api: migrateAPIElements,
|
||||
} as const;
|
||||
|
||||
for (const scope of ALL_SCOPES) {
|
||||
const migrated = migrationsByScope[scope](
|
||||
[frame],
|
||||
SCHEMA_VERSIONS.initial,
|
||||
)!;
|
||||
expect(migrated[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
);
|
||||
}
|
||||
const migrated = migrateElements([frame])!;
|
||||
expect(migrated[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
);
|
||||
});
|
||||
|
||||
it("should resolve invalid schema versions using fallback", () => {
|
||||
expect(resolveSchemaVersion(undefined, SCHEMA_VERSIONS.initial)).toBe(
|
||||
SCHEMA_VERSIONS.initial,
|
||||
);
|
||||
expect(resolveSchemaVersion(0, SCHEMA_VERSIONS.latest)).toBe(
|
||||
SCHEMA_VERSIONS.latest,
|
||||
);
|
||||
expect(resolveSchemaVersion(2, SCHEMA_VERSIONS.initial)).toBe(2);
|
||||
it("should resolve invalid schema versions to initial", () => {
|
||||
expect(resolveSchemaVersion(undefined)).toBe(SCHEMA_VERSIONS.initial);
|
||||
expect(resolveSchemaVersion(0)).toBe(SCHEMA_VERSIONS.initial);
|
||||
expect(resolveSchemaVersion(2)).toBe(2);
|
||||
});
|
||||
|
||||
it("should have a valid migration registry configuration", () => {
|
||||
@@ -85,14 +76,12 @@ describe("schema migration", () => {
|
||||
version: 2.1,
|
||||
title: "",
|
||||
description: " ",
|
||||
scope: [],
|
||||
apply: (elements) => elements,
|
||||
},
|
||||
{
|
||||
version: 2.1,
|
||||
title: "duplicate",
|
||||
description: "duplicate version",
|
||||
scope: ["scene"],
|
||||
apply: (elements) => elements,
|
||||
},
|
||||
];
|
||||
@@ -112,7 +101,6 @@ describe("schema migration", () => {
|
||||
version: SCHEMA_VERSIONS.initial,
|
||||
title: "invalid start",
|
||||
description: "bad version",
|
||||
scope: ["scene"],
|
||||
apply: (elements) => elements,
|
||||
},
|
||||
]);
|
||||
@@ -126,7 +114,6 @@ describe("schema migration", () => {
|
||||
version: SCHEMA_VERSIONS.latest + 1,
|
||||
title: "future migration",
|
||||
description: "future migration for test",
|
||||
scope: ["scene"],
|
||||
apply: (elements) => elements,
|
||||
},
|
||||
]);
|
||||
@@ -137,23 +124,20 @@ describe("schema migration", () => {
|
||||
});
|
||||
|
||||
it("should not depend on temporary fields during migration", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#a5d8ff",
|
||||
});
|
||||
const frame = {
|
||||
...API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#a5d8ff",
|
||||
}),
|
||||
schemaVersion: undefined,
|
||||
};
|
||||
const withTempField = {
|
||||
...frame,
|
||||
backgroundEnabled: false,
|
||||
} as typeof frame & { backgroundEnabled: boolean };
|
||||
|
||||
const migratedBase = migrateSceneElements(
|
||||
[frame],
|
||||
SCHEMA_VERSIONS.initial,
|
||||
)!;
|
||||
const migratedWithTempField = migrateSceneElements(
|
||||
[withTempField],
|
||||
SCHEMA_VERSIONS.initial,
|
||||
)!;
|
||||
const migratedBase = migrateElements([frame])!;
|
||||
const migratedWithTempField = migrateElements([withTempField])!;
|
||||
|
||||
expect(migratedBase[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
@@ -163,7 +147,7 @@ describe("schema migration", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should use per-element schema when payload schema is missing", () => {
|
||||
it("should use per-element schema hints", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ff0000",
|
||||
@@ -173,19 +157,19 @@ describe("schema migration", () => {
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof frame & { schemaVersion: number };
|
||||
|
||||
const migrated = migrateClipboardElements([frameFromModernSource], {
|
||||
payloadSchemaVersion: undefined,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
})!;
|
||||
const migrated = migrateElements([frameFromModernSource])!;
|
||||
|
||||
expect(migrated[0].backgroundColor).toBe("#ff0000");
|
||||
});
|
||||
|
||||
it("should migrate mixed-hint elements individually when payload schema is missing", () => {
|
||||
const legacyFrame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ff0000",
|
||||
});
|
||||
it("should migrate mixed-hint elements individually", () => {
|
||||
const legacyFrame = {
|
||||
...API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ff0000",
|
||||
}),
|
||||
schemaVersion: undefined,
|
||||
};
|
||||
const modernFrame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#00ff00",
|
||||
@@ -195,10 +179,7 @@ describe("schema migration", () => {
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof modernFrame & { schemaVersion: number };
|
||||
|
||||
const migrated = migrateSceneElements([legacyFrame, modernFrameWithHint], {
|
||||
payloadSchemaVersion: undefined,
|
||||
fallbackVersion: SCHEMA_VERSIONS.initial,
|
||||
})!;
|
||||
const migrated = migrateElements([legacyFrame, modernFrameWithHint])!;
|
||||
|
||||
expect(migrated[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
@@ -206,34 +187,20 @@ describe("schema migration", () => {
|
||||
expect(migrated[1].backgroundColor).toBe("#00ff00");
|
||||
});
|
||||
|
||||
it("should prefer payload schema over per-element schema", () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
backgroundColor: "#ff0000",
|
||||
});
|
||||
const frameFromModernSource = {
|
||||
...frame,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof frame & { schemaVersion: number };
|
||||
|
||||
const migrated = migrateClipboardElements([frameFromModernSource], {
|
||||
payloadSchemaVersion: SCHEMA_VERSIONS.initial,
|
||||
fallbackVersion: SCHEMA_VERSIONS.latest,
|
||||
})!;
|
||||
|
||||
expect(migrated[0].backgroundColor).toBe(
|
||||
DEFAULT_ELEMENT_PROPS.backgroundColor,
|
||||
);
|
||||
it("should stamp schemaVersion to latest after migration", () => {
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
const migrated = migrateElements([rect])!;
|
||||
expect(migrated[0].schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
||||
});
|
||||
|
||||
it("should detect schema hints on elements", () => {
|
||||
const frame = API.createElement({ type: "frame" });
|
||||
const withHint = {
|
||||
...frame,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest,
|
||||
} as typeof frame & { schemaVersion: number };
|
||||
it("should preserve higher-than-latest schema versions", () => {
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
const futureRect = {
|
||||
...rect,
|
||||
schemaVersion: SCHEMA_VERSIONS.latest + 1,
|
||||
} as typeof rect & { schemaVersion: number };
|
||||
|
||||
expect(hasElementSchemaVersion([frame])).toBe(false);
|
||||
expect(hasElementSchemaVersion([withHint])).toBe(true);
|
||||
const migrated = migrateElements([futureRect])!;
|
||||
expect(migrated[0].schemaVersion).toBe(SCHEMA_VERSIONS.latest + 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import type { NormalizedZoomValue } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { API } from "../helpers/api";
|
||||
import * as restore from "../../data/restore";
|
||||
import { SCHEMA_VERSIONS } from "../../data/schema";
|
||||
import { getDefaultAppState } from "../../appState";
|
||||
|
||||
import type { ImportedDataState } from "../../data/types";
|
||||
@@ -131,18 +130,21 @@ describe("restoreElements", () => {
|
||||
type: "frame",
|
||||
backgroundColor: "#a5d8ff",
|
||||
});
|
||||
const legacyFrame = {
|
||||
...frame,
|
||||
schemaVersion: undefined,
|
||||
} as typeof frame & { schemaVersion?: number };
|
||||
|
||||
const restoredLibraryItems = restore.restoreLibraryItems(
|
||||
[
|
||||
{
|
||||
id: "library-item-1",
|
||||
status: "published",
|
||||
elements: [frame],
|
||||
elements: [legacyFrame],
|
||||
created: Date.now(),
|
||||
},
|
||||
],
|
||||
"published",
|
||||
SCHEMA_VERSIONS.initial,
|
||||
);
|
||||
|
||||
expect(
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { ExcalidrawGenericElement } from "@excalidraw/element/types";
|
||||
import { parseLibraryJSON } from "../data/blob";
|
||||
import { serializeLibraryAsJSON } from "../data/json";
|
||||
import { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||
import { SCHEMA_VERSIONS } from "../data/schema";
|
||||
import { Excalidraw } from "../index";
|
||||
|
||||
import { API } from "./helpers/api";
|
||||
@@ -168,11 +167,10 @@ describe("library", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should include schema version when serializing library", () => {
|
||||
it("should serialize library payload", () => {
|
||||
const serialized = serializeLibraryAsJSON([]);
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed.schemaVersion).toBe(SCHEMA_VERSIONS.latest);
|
||||
expect(parsed.type).toBe("excalidrawlib");
|
||||
});
|
||||
|
||||
// NOTE: mocked to test logic, not actual drag&drop via UI
|
||||
|
||||
Reference in New Issue
Block a user