683 lines
24 KiB
TypeScript
683 lines
24 KiB
TypeScript
import { vi } from "vitest";
|
|
import fallbackLangData from "./helpers/locales/en.json";
|
|
import {
|
|
SubtypeLoadedCb,
|
|
SubtypeRecord,
|
|
SubtypeMethods,
|
|
SubtypePrepFn,
|
|
addSubtypeMethods,
|
|
ensureSubtypesLoadedForElements,
|
|
getSubtypeMethods,
|
|
getSubtypeNames,
|
|
hasAlwaysEnabledActions,
|
|
isValidSubtype,
|
|
selectSubtype,
|
|
subtypeCollides,
|
|
} from "../element/subtypes";
|
|
|
|
import { render } from "./test-utils";
|
|
import { API } from "./helpers/api";
|
|
import { Excalidraw } from "../packages/excalidraw/index";
|
|
|
|
import {
|
|
ExcalidrawElement,
|
|
ExcalidrawTextElement,
|
|
FontString,
|
|
Theme,
|
|
} from "../element/types";
|
|
import { createIcon, iconFillColor } from "../components/icons";
|
|
import { SubtypeButton } from "../components/Subtypes";
|
|
import { LangLdr, registerCustomLangData } from "../i18n";
|
|
import { getFontString, getShortcutKey } from "../utils";
|
|
import * as textElementUtils from "../element/textElement";
|
|
import { isTextElement } from "../element";
|
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
|
import { Action, ActionName, makeCustomActionName } from "../actions/types";
|
|
import { AppState } from "../types";
|
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
|
import { actionChangeSloppiness } from "../actions";
|
|
import { actionChangeRoundness } from "../actions/actionProperties";
|
|
|
|
const MW = 200;
|
|
const TWIDTH = 200;
|
|
const THEIGHT = 20;
|
|
const TBASELINE = 0;
|
|
const FONTSIZE = 20;
|
|
const DBFONTSIZE = 40;
|
|
const TRFONTSIZE = 60;
|
|
|
|
const getLangData: LangLdr = (langCode) =>
|
|
import(`./helpers/locales/${langCode}.json`);
|
|
|
|
const testSubtypeIcon = ({ theme }: { theme: Theme }) =>
|
|
createIcon(
|
|
<path
|
|
stroke={iconFillColor(theme)}
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
fill="none"
|
|
/>,
|
|
{ width: 40, height: 20, mirror: true },
|
|
);
|
|
|
|
const TEST_ACTION = "testAction";
|
|
const TEST_DISABLE1 = actionChangeSloppiness;
|
|
const TEST_DISABLE3 = actionChangeRoundness;
|
|
|
|
const test1: SubtypeRecord = {
|
|
subtype: "test",
|
|
parents: ["line", "arrow", "rectangle", "diamond", "ellipse"],
|
|
disabledNames: [TEST_DISABLE1.name as ActionName],
|
|
actionNames: [TEST_ACTION],
|
|
};
|
|
|
|
const testAction: Action = {
|
|
name: makeCustomActionName(TEST_ACTION),
|
|
trackEvent: false,
|
|
perform: (elements, appState) => {
|
|
return {
|
|
elements,
|
|
commitToHistory: false,
|
|
};
|
|
},
|
|
};
|
|
|
|
const test1Button = SubtypeButton(
|
|
test1.subtype,
|
|
test1.parents[0],
|
|
testSubtypeIcon,
|
|
);
|
|
const test1NonParent = "text" as const;
|
|
|
|
const test2: SubtypeRecord = {
|
|
subtype: "test2",
|
|
parents: ["text"],
|
|
};
|
|
|
|
const test2Button = SubtypeButton(
|
|
test2.subtype,
|
|
test2.parents[0],
|
|
testSubtypeIcon,
|
|
);
|
|
|
|
const test3: SubtypeRecord = {
|
|
subtype: "test3",
|
|
parents: ["text", "line"],
|
|
shortcutMap: {
|
|
testShortcut: [getShortcutKey("Shift+T")],
|
|
},
|
|
alwaysEnabledNames: ["test3Always"],
|
|
disabledNames: [TEST_DISABLE3.name as ActionName],
|
|
};
|
|
|
|
const test3Button = SubtypeButton(
|
|
test3.subtype,
|
|
test3.parents[0],
|
|
testSubtypeIcon,
|
|
);
|
|
|
|
const cleanTestElementUpdate = function (updates) {
|
|
const oldUpdates = {};
|
|
for (const key in updates) {
|
|
if (key !== "roughness") {
|
|
(oldUpdates as any)[key] = (updates as any)[key];
|
|
}
|
|
}
|
|
(updates as any).roughness = 0;
|
|
return oldUpdates;
|
|
} as SubtypeMethods["clean"];
|
|
|
|
const prepareNullSubtype = function () {
|
|
const methods = {} as SubtypeMethods;
|
|
methods.clean = cleanTestElementUpdate;
|
|
methods.measureText = measureTest2;
|
|
methods.wrapText = wrapTest2;
|
|
|
|
const actions = [test1Button, test2Button, test3Button];
|
|
return { actions, methods };
|
|
} as SubtypePrepFn;
|
|
|
|
const prepareTest1Subtype = function (
|
|
addSubtypeAction,
|
|
addLangData,
|
|
onSubtypeLoaded,
|
|
) {
|
|
const methods = {} as SubtypeMethods;
|
|
methods.clean = cleanTestElementUpdate;
|
|
|
|
addLangData(fallbackLangData, getLangData);
|
|
registerCustomLangData(fallbackLangData, getLangData);
|
|
|
|
const actions = [testAction, test1Button];
|
|
actions.forEach((action) => addSubtypeAction(action));
|
|
|
|
return { actions, methods };
|
|
} as SubtypePrepFn;
|
|
|
|
let test2Loaded = false;
|
|
|
|
const ensureLoadedTest2: SubtypeMethods["ensureLoaded"] = async (callback) => {
|
|
test2Loaded = true;
|
|
if (onTest2Loaded) {
|
|
onTest2Loaded((el) => isTextElement(el) && el.subtype === test2.subtype);
|
|
}
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
};
|
|
|
|
const measureTest2: SubtypeMethods["measureText"] = function (element, next) {
|
|
const text = next?.text ?? element.text;
|
|
const customData = next?.customData ?? {};
|
|
const fontSize = customData.triple
|
|
? TRFONTSIZE
|
|
: next?.fontSize ?? element.fontSize;
|
|
const fontFamily = element.fontFamily;
|
|
const fontString = getFontString({ fontSize, fontFamily });
|
|
const lineHeight = element.lineHeight;
|
|
const metrics = textElementUtils.measureText(text, fontString, lineHeight);
|
|
const width = test2Loaded
|
|
? metrics.width * 2
|
|
: Math.max(metrics.width - 10, 0);
|
|
const height = test2Loaded
|
|
? metrics.height * 2
|
|
: Math.max(metrics.height - 5, 0);
|
|
return { width, height, baseline: 1 };
|
|
};
|
|
|
|
const wrapTest2: SubtypeMethods["wrapText"] = function (
|
|
element,
|
|
maxWidth,
|
|
next,
|
|
) {
|
|
const text = next?.text ?? element.originalText;
|
|
if (next?.customData && next?.customData.triple === true) {
|
|
return `${text.split(" ").join("\n")}\nHELLO WORLD.`;
|
|
}
|
|
if (next?.fontSize === DBFONTSIZE) {
|
|
return `${text.split(" ").join("\n")}\nHELLO World.`;
|
|
}
|
|
return `${text.split(" ").join("\n")}\nHello world.`;
|
|
};
|
|
|
|
let onTest2Loaded: SubtypeLoadedCb | undefined;
|
|
|
|
const prepareTest2Subtype = function (
|
|
addSubtypeAction,
|
|
addLangData,
|
|
onSubtypeLoaded,
|
|
) {
|
|
const methods = {
|
|
ensureLoaded: ensureLoadedTest2,
|
|
measureText: measureTest2,
|
|
wrapText: wrapTest2,
|
|
} as SubtypeMethods;
|
|
|
|
addLangData(fallbackLangData, getLangData);
|
|
registerCustomLangData(fallbackLangData, getLangData);
|
|
|
|
const actions = [test2Button];
|
|
actions.forEach((action) => addSubtypeAction(action));
|
|
|
|
onTest2Loaded = onSubtypeLoaded;
|
|
|
|
return { actions, methods };
|
|
} as SubtypePrepFn;
|
|
|
|
const prepareTest3Subtype = function (
|
|
addSubtypeAction,
|
|
addLangData,
|
|
onSubtypeLoaded,
|
|
) {
|
|
const methods = {} as SubtypeMethods;
|
|
|
|
addLangData(fallbackLangData, getLangData);
|
|
registerCustomLangData(fallbackLangData, getLangData);
|
|
|
|
const actions = [test3Button];
|
|
actions.forEach((action) => addSubtypeAction(action));
|
|
|
|
return { actions, methods };
|
|
} as SubtypePrepFn;
|
|
|
|
const { h } = window;
|
|
|
|
describe("subtype registration", () => {
|
|
it("should check for invalid subtype or parents", async () => {
|
|
await render(<Excalidraw />, {});
|
|
// Define invalid subtype records
|
|
const null1 = {} as SubtypeRecord;
|
|
const null2 = { subtype: "" } as SubtypeRecord;
|
|
const null3 = { subtype: "null" } as SubtypeRecord;
|
|
const null4 = { subtype: "null", parents: [] } as SubtypeRecord;
|
|
// Try registering the invalid subtypes
|
|
const prepN1 = API.addSubtype(null1, prepareNullSubtype);
|
|
const prepN2 = API.addSubtype(null2, prepareNullSubtype);
|
|
const prepN3 = API.addSubtype(null3, prepareNullSubtype);
|
|
const prepN4 = API.addSubtype(null4, prepareNullSubtype);
|
|
// Verify the guards in `prepareSubtype` worked
|
|
expect(prepN1).toStrictEqual({ actions: null, methods: {} });
|
|
expect(prepN2).toStrictEqual({ actions: null, methods: {} });
|
|
expect(prepN3).toStrictEqual({ actions: null, methods: {} });
|
|
expect(prepN4).toStrictEqual({ actions: null, methods: {} });
|
|
});
|
|
it("should return subtype actions and methods correctly", async () => {
|
|
// Check initial registration works
|
|
let prep1 = API.addSubtype(test1, prepareTest1Subtype);
|
|
expect(prep1.actions).toStrictEqual([testAction, test1Button]);
|
|
expect(prep1.methods).toStrictEqual({ clean: cleanTestElementUpdate });
|
|
// Check repeat registration fails
|
|
prep1 = API.addSubtype(test1, prepareNullSubtype);
|
|
expect(prep1.actions).toBeNull();
|
|
expect(prep1.methods).toStrictEqual({ clean: cleanTestElementUpdate });
|
|
|
|
// Check initial registration works
|
|
let prep2 = API.addSubtype(test2, prepareTest2Subtype);
|
|
expect(prep2.actions).toStrictEqual([test2Button]);
|
|
expect(prep2.methods).toStrictEqual({
|
|
ensureLoaded: ensureLoadedTest2,
|
|
measureText: measureTest2,
|
|
wrapText: wrapTest2,
|
|
});
|
|
// Check repeat registration fails
|
|
prep2 = API.addSubtype(test2, prepareNullSubtype);
|
|
expect(prep2.actions).toBeNull();
|
|
expect(prep2.methods).toStrictEqual({
|
|
ensureLoaded: ensureLoadedTest2,
|
|
measureText: measureTest2,
|
|
wrapText: wrapTest2,
|
|
});
|
|
|
|
// Check initial registration works
|
|
let prep3 = API.addSubtype(test3, prepareTest3Subtype);
|
|
expect(prep3.actions).toStrictEqual([test3Button]);
|
|
expect(prep3.methods).toStrictEqual({});
|
|
// Check repeat registration fails
|
|
prep3 = API.addSubtype(test3, prepareNullSubtype);
|
|
expect(prep3.actions).toBeNull();
|
|
expect(prep3.methods).toStrictEqual({});
|
|
});
|
|
});
|
|
|
|
describe("subtypes", () => {
|
|
it("should correctly register", async () => {
|
|
const subtypes = getSubtypeNames();
|
|
expect(subtypes).toContain(test1.subtype);
|
|
expect(subtypes).toContain(test2.subtype);
|
|
expect(subtypes).toContain(test3.subtype);
|
|
});
|
|
it("should return subtype methods", async () => {
|
|
expect(getSubtypeMethods(undefined)).toBeUndefined();
|
|
const test1Methods = getSubtypeMethods(test1.subtype);
|
|
expect(test1Methods?.clean).toBeDefined();
|
|
expect(test1Methods?.render).toBeUndefined();
|
|
expect(test1Methods?.wrapText).toBeUndefined();
|
|
expect(test1Methods?.renderSvg).toBeUndefined();
|
|
expect(test1Methods?.measureText).toBeUndefined();
|
|
expect(test1Methods?.ensureLoaded).toBeUndefined();
|
|
});
|
|
it("should not overwrite subtype methods", async () => {
|
|
addSubtypeMethods(test1.subtype, {});
|
|
addSubtypeMethods(test2.subtype, {});
|
|
addSubtypeMethods(test3.subtype, { clean: cleanTestElementUpdate });
|
|
const test1Methods = getSubtypeMethods(test1.subtype);
|
|
expect(test1Methods?.clean).toBeDefined();
|
|
const test2Methods = getSubtypeMethods(test2.subtype);
|
|
expect(test2Methods?.measureText).toBeDefined();
|
|
expect(test2Methods?.wrapText).toBeDefined();
|
|
const test3Methods = getSubtypeMethods(test3.subtype);
|
|
expect(test3Methods?.clean).toBeUndefined();
|
|
});
|
|
it("should register custom shortcuts", async () => {
|
|
expect(
|
|
getShortcutFromShortcutName(makeCustomActionName("testShortcut")),
|
|
).toBe("Shift+T");
|
|
});
|
|
it("should correctly validate", async () => {
|
|
test1.parents.forEach((p) => {
|
|
expect(isValidSubtype(test1.subtype, p)).toBe(true);
|
|
expect(isValidSubtype(undefined, p)).toBe(false);
|
|
});
|
|
expect(isValidSubtype(test1.subtype, test1NonParent)).toBe(false);
|
|
expect(isValidSubtype(test1.subtype, undefined)).toBe(false);
|
|
expect(isValidSubtype(undefined, undefined)).toBe(false);
|
|
});
|
|
it("should collide with themselves", async () => {
|
|
expect(subtypeCollides(test1.subtype, [test1.subtype])).toBe(true);
|
|
expect(subtypeCollides(test1.subtype, [test1.subtype, test2.subtype])).toBe(
|
|
true,
|
|
);
|
|
});
|
|
it("should not collide without type overlap", async () => {
|
|
expect(subtypeCollides(test1.subtype, [test2.subtype])).toBe(false);
|
|
});
|
|
it("should collide with type overlap", async () => {
|
|
expect(subtypeCollides(test1.subtype, [test3.subtype])).toBe(true);
|
|
});
|
|
it("should apply to ExcalidrawElements", async () => {
|
|
const elements = [
|
|
API.createElement({ type: "line", id: "A", subtype: test1.subtype }),
|
|
API.createElement({ type: "arrow", id: "B", subtype: test1.subtype }),
|
|
API.createElement({ type: "rectangle", id: "C", subtype: test1.subtype }),
|
|
API.createElement({ type: "diamond", id: "D", subtype: test1.subtype }),
|
|
API.createElement({ type: "ellipse", id: "E", subtype: test1.subtype }),
|
|
];
|
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
|
elements.forEach((el) => expect(el.subtype).toBe(test1.subtype));
|
|
});
|
|
it("should enforce prop value restrictions", async () => {
|
|
const elements = [
|
|
API.createElement({
|
|
type: "line",
|
|
id: "A",
|
|
subtype: test1.subtype,
|
|
roughness: 1,
|
|
}),
|
|
API.createElement({ type: "line", id: "B", roughness: 1 }),
|
|
];
|
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
|
elements.forEach((el) => {
|
|
if (el.subtype === test1.subtype) {
|
|
expect(el.roughness).toBe(0);
|
|
} else {
|
|
expect(el.roughness).toBe(1);
|
|
}
|
|
});
|
|
});
|
|
it("should consider enforced prop values in version increments", async () => {
|
|
const rectA = API.createElement({
|
|
type: "line",
|
|
id: "A",
|
|
subtype: test1.subtype,
|
|
roughness: 1,
|
|
strokeWidth: 1,
|
|
});
|
|
const rectB = API.createElement({
|
|
type: "line",
|
|
id: "B",
|
|
subtype: test1.subtype,
|
|
roughness: 1,
|
|
strokeWidth: 1,
|
|
});
|
|
// Initial element creation checks
|
|
expect(rectA.roughness).toBe(0);
|
|
expect(rectB.roughness).toBe(0);
|
|
expect(rectA.version).toBe(1);
|
|
expect(rectB.version).toBe(1);
|
|
// Check that attempting to set prop values not permitted by the subtype
|
|
// doesn't increment element versions
|
|
mutateElement(rectA, { roughness: 2 });
|
|
mutateElement(rectB, { roughness: 2, strokeWidth: 2 });
|
|
expect(rectA.version).toBe(1);
|
|
expect(rectB.version).toBe(2);
|
|
// Check that element versions don't increment when creating new elements
|
|
// while attempting to use prop values not permitted by the subtype
|
|
// First check based on `rectA` (unsuccessfully mutated)
|
|
const rectC = newElementWith(rectA, { roughness: 1 });
|
|
const rectD = newElementWith(rectA, { roughness: 1, strokeWidth: 1.5 });
|
|
expect(rectC.version).toBe(1);
|
|
expect(rectD.version).toBe(2);
|
|
// Then check based on `rectB` (successfully mutated)
|
|
const rectE = newElementWith(rectB, { roughness: 1 });
|
|
const rectF = newElementWith(rectB, { roughness: 1, strokeWidth: 1.5 });
|
|
expect(rectE.version).toBe(2);
|
|
expect(rectF.version).toBe(3);
|
|
});
|
|
it("should call custom text methods", async () => {
|
|
const testString = "A quick brown fox jumps over the lazy dog.";
|
|
const elements = [
|
|
API.createElement({
|
|
type: "text",
|
|
id: "A",
|
|
subtype: test2.subtype,
|
|
text: testString,
|
|
fontSize: FONTSIZE,
|
|
}),
|
|
];
|
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
|
const mockMeasureText = (text: string, font: FontString) => {
|
|
if (text === testString) {
|
|
let multiplier = 1;
|
|
if (font.includes(`${DBFONTSIZE}`)) {
|
|
multiplier = 2;
|
|
}
|
|
if (font.includes(`${TRFONTSIZE}`)) {
|
|
multiplier = 3;
|
|
}
|
|
const width = multiplier * TWIDTH;
|
|
const height = multiplier * THEIGHT;
|
|
const baseline = multiplier * TBASELINE;
|
|
return { width, height, baseline };
|
|
}
|
|
return { width: 1, height: 0, baseline: 0 };
|
|
};
|
|
|
|
vi.spyOn(textElementUtils, "measureText").mockImplementation(
|
|
mockMeasureText,
|
|
);
|
|
|
|
elements.forEach((el) => {
|
|
if (isTextElement(el)) {
|
|
// First test with `ExcalidrawTextElement.text`
|
|
const metrics = textElementUtils.measureTextElement(el);
|
|
expect(metrics).toStrictEqual({
|
|
width: TWIDTH - 10,
|
|
height: THEIGHT - 5,
|
|
baseline: TBASELINE + 1,
|
|
});
|
|
const wrappedText = textElementUtils.wrapTextElement(el, MW);
|
|
expect(wrappedText).toEqual(
|
|
`${testString.split(" ").join("\n")}\nHello world.`,
|
|
);
|
|
|
|
// Now test with modified text in `next`
|
|
let next: {
|
|
text?: string;
|
|
fontSize?: number;
|
|
customData?: Record<string, any>;
|
|
} = {
|
|
text: "Hello world.",
|
|
};
|
|
const nextMetrics = textElementUtils.measureTextElement(el, next);
|
|
expect(nextMetrics).toStrictEqual({ width: 0, height: 0, baseline: 1 });
|
|
const nextWrappedText = textElementUtils.wrapTextElement(el, MW, next);
|
|
expect(nextWrappedText).toEqual("Hello\nworld.\nHello world.");
|
|
|
|
// Now test modified fontSizes in `next`
|
|
next = { fontSize: DBFONTSIZE };
|
|
const nextFM = textElementUtils.measureTextElement(el, next);
|
|
expect(nextFM).toStrictEqual({
|
|
width: 2 * TWIDTH - 10,
|
|
height: 2 * THEIGHT - 5,
|
|
baseline: 2 * TBASELINE + 1,
|
|
});
|
|
const nextFWrText = textElementUtils.wrapTextElement(el, MW, next);
|
|
expect(nextFWrText).toEqual(
|
|
`${testString.split(" ").join("\n")}\nHELLO World.`,
|
|
);
|
|
|
|
// Now test customData in `next`
|
|
next = { customData: { triple: true } };
|
|
const nextCD = textElementUtils.measureTextElement(el, next);
|
|
expect(nextCD).toStrictEqual({
|
|
width: 3 * TWIDTH - 10,
|
|
height: 3 * THEIGHT - 5,
|
|
baseline: 3 * TBASELINE + 1,
|
|
});
|
|
const nextCDWrText = textElementUtils.wrapTextElement(el, MW, next);
|
|
expect(nextCDWrText).toEqual(
|
|
`${testString.split(" ").join("\n")}\nHELLO WORLD.`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
it("should recognize subtypes with always-enabled actions", async () => {
|
|
expect(hasAlwaysEnabledActions(test1.subtype)).toBe(false);
|
|
expect(hasAlwaysEnabledActions(test2.subtype)).toBe(false);
|
|
expect(hasAlwaysEnabledActions(test3.subtype)).toBe(true);
|
|
});
|
|
it("should select active subtypes and customData", async () => {
|
|
const appState = {} as {
|
|
activeSubtypes: AppState["activeSubtypes"];
|
|
customData: AppState["customData"];
|
|
};
|
|
|
|
// No active subtypes
|
|
let subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.subtype).toBeUndefined();
|
|
expect(subtypes.customData).toBeUndefined();
|
|
// Subtype for both "text" and "line" types
|
|
appState.activeSubtypes = [test3.subtype];
|
|
subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.subtype).toBe(test3.subtype);
|
|
subtypes = selectSubtype(appState, "line");
|
|
expect(subtypes.subtype).toBe(test3.subtype);
|
|
subtypes = selectSubtype(appState, "arrow");
|
|
expect(subtypes.subtype).toBeUndefined();
|
|
// Subtype for multiple linear types
|
|
appState.activeSubtypes = [test1.subtype];
|
|
subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.subtype).toBeUndefined();
|
|
subtypes = selectSubtype(appState, "line");
|
|
expect(subtypes.subtype).toBe(test1.subtype);
|
|
subtypes = selectSubtype(appState, "arrow");
|
|
expect(subtypes.subtype).toBe(test1.subtype);
|
|
// Subtype for "text" only
|
|
appState.activeSubtypes = [test2.subtype];
|
|
subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.subtype).toBe(test2.subtype);
|
|
subtypes = selectSubtype(appState, "line");
|
|
expect(subtypes.subtype).toBeUndefined();
|
|
subtypes = selectSubtype(appState, "arrow");
|
|
expect(subtypes.subtype).toBeUndefined();
|
|
|
|
// Test customData
|
|
appState.customData = {};
|
|
appState.customData[test1.subtype] = { test: true };
|
|
appState.customData[test2.subtype] = { test2: true };
|
|
appState.customData[test3.subtype] = { test3: true };
|
|
// Subtype for both "text" and "line" types
|
|
appState.activeSubtypes = [test3.subtype];
|
|
subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.customData).toBeDefined();
|
|
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test3.subtype]).toBe(true);
|
|
subtypes = selectSubtype(appState, "line");
|
|
expect(subtypes.customData).toBeDefined();
|
|
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test3.subtype]).toBe(true);
|
|
subtypes = selectSubtype(appState, "arrow");
|
|
expect(subtypes.customData).toBeUndefined();
|
|
// Subtype for multiple linear types
|
|
appState.activeSubtypes = [test1.subtype];
|
|
subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.customData).toBeUndefined();
|
|
subtypes = selectSubtype(appState, "line");
|
|
expect(subtypes.customData).toBeDefined();
|
|
expect(subtypes.customData![test1.subtype]).toBe(true);
|
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
|
// Multiple, non-colliding subtypes
|
|
appState.activeSubtypes = [test1.subtype, test2.subtype];
|
|
subtypes = selectSubtype(appState, "text");
|
|
expect(subtypes.customData).toBeDefined();
|
|
expect(subtypes.customData![test1.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test2.subtype]).toBe(true);
|
|
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
|
subtypes = selectSubtype(appState, "line");
|
|
expect(subtypes.customData).toBeDefined();
|
|
expect(subtypes.customData![test1.subtype]).toBe(true);
|
|
expect(subtypes.customData![test2.subtype]).toBeUndefined();
|
|
expect(subtypes.customData![test3.subtype]).toBeUndefined();
|
|
});
|
|
});
|
|
describe("subtype actions", () => {
|
|
let elements: ExcalidrawElement[];
|
|
beforeEach(async () => {
|
|
elements = [
|
|
API.createElement({ type: "line", id: "A", subtype: test1.subtype }),
|
|
API.createElement({ type: "line", id: "B" }),
|
|
API.createElement({ type: "line", id: "C", subtype: test3.subtype }),
|
|
API.createElement({ type: "text", id: "D", subtype: test3.subtype }),
|
|
];
|
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
|
});
|
|
it("should apply to elements with their subtype", async () => {
|
|
h.setState({ selectedElementIds: { A: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(false);
|
|
});
|
|
it("should apply to elements without a subtype", async () => {
|
|
h.setState({ selectedElementIds: { B: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(false);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
|
});
|
|
it("should apply to elements with and without their subtype", async () => {
|
|
h.setState({ selectedElementIds: { A: true, B: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
|
});
|
|
it("should apply to elements with a different subtype", async () => {
|
|
h.setState({ selectedElementIds: { C: true, D: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(false);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
|
});
|
|
it("should apply to like types with varying subtypes", async () => {
|
|
h.setState({ selectedElementIds: { A: true, C: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
|
});
|
|
it("should apply to non-like types with varying subtypes", async () => {
|
|
h.setState({ selectedElementIds: { A: true, D: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(false);
|
|
});
|
|
it("should apply to like/non-like types with varying subtypes", async () => {
|
|
h.setState({ selectedElementIds: { A: true, B: true, D: true } });
|
|
const am = h.app.actionManager;
|
|
expect(am.isActionEnabled(testAction, { elements })).toBe(true);
|
|
expect(am.isActionEnabled(TEST_DISABLE1, { elements })).toBe(true);
|
|
});
|
|
it("should apply to the correct parent type", async () => {
|
|
const am = h.app.actionManager;
|
|
h.setState({ selectedElementIds: { A: true, C: true } });
|
|
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
|
h.setState({ selectedElementIds: { A: true, D: true } });
|
|
expect(am.isActionEnabled(TEST_DISABLE3, { elements })).toBe(true);
|
|
});
|
|
});
|
|
describe("subtype loading", () => {
|
|
let elements: ExcalidrawElement[];
|
|
beforeEach(async () => {
|
|
const testString = "A quick brown fox jumps over the lazy dog.";
|
|
elements = [
|
|
API.createElement({
|
|
type: "text",
|
|
id: "A",
|
|
subtype: test2.subtype,
|
|
text: testString,
|
|
}),
|
|
];
|
|
await render(<Excalidraw />, { localStorageData: { elements } });
|
|
h.elements = elements;
|
|
});
|
|
it("should redraw text bounding boxes", async () => {
|
|
h.setState({ selectedElementIds: { A: true } });
|
|
const el = h.elements[0] as ExcalidrawTextElement;
|
|
expect(el.width).toEqual(100);
|
|
expect(el.height).toEqual(100);
|
|
ensureSubtypesLoadedForElements(elements);
|
|
expect(el.width).toEqual(TWIDTH * 2);
|
|
expect(el.height).toEqual(THEIGHT * 2);
|
|
expect(el.baseline).toEqual(TBASELINE + 1);
|
|
});
|
|
});
|