mirror of
https://github.com/penpot/penpot.git
synced 2026-09-22 11:56:15 +00:00
✨ Add end-to-end tests for plugins validation
This commit is contained in:
parent
7d372769c3
commit
8a3aecd75a
@ -156,4 +156,23 @@ describe.skipIfMocked('Comments', () => {
|
|||||||
cleanup(thread);
|
cleanup(thread);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const content of [' ', '\n\t', '\u200b']) {
|
||||||
|
test(`blank comment content ${JSON.stringify(content)} rejects everywhere`, async (ctx) => {
|
||||||
|
const p = page(ctx);
|
||||||
|
await expectReject(() => p.addCommentThread(content, { x: 0, y: 0 }));
|
||||||
|
|
||||||
|
const thread = await p.addCommentThread('parent', { x: 12, y: 12 });
|
||||||
|
try {
|
||||||
|
await expectReject(() => thread.reply(content));
|
||||||
|
const comments = await thread.findComments();
|
||||||
|
expect(comments.length).toBeGreaterThan(0);
|
||||||
|
expect(() => {
|
||||||
|
comments[0].content = content;
|
||||||
|
}).toThrow();
|
||||||
|
} finally {
|
||||||
|
cleanup(thread);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -222,13 +222,21 @@ describe('Component instances', () => {
|
|||||||
const inst = comp.instance();
|
const inst = comp.instance();
|
||||||
ctx.board.appendChild(inst);
|
ctx.board.appendChild(inst);
|
||||||
|
|
||||||
const mainColor = main.fills?.[0]?.fillColor;
|
const mainFill = main.fills?.[0];
|
||||||
|
const mainColor =
|
||||||
|
typeof mainFill === 'string' ? mainFill : mainFill?.fillColor;
|
||||||
inst.fills = [{ fillColor: '#FF0000', fillOpacity: 1 }];
|
inst.fills = [{ fillColor: '#FF0000', fillOpacity: 1 }];
|
||||||
// The override applied (fill getter normalizes to lowercase).
|
// The override applied (fill getter normalizes to lowercase).
|
||||||
expect(inst.fills?.[0]?.fillColor?.toLowerCase()).toBe('#ff0000');
|
const overrideFill = inst.fills?.[0];
|
||||||
|
const overrideColor =
|
||||||
|
typeof overrideFill === 'string' ? overrideFill : overrideFill?.fillColor;
|
||||||
|
expect(overrideColor?.toLowerCase()).toBe('#ff0000');
|
||||||
|
|
||||||
inst.resetOverrides();
|
inst.resetOverrides();
|
||||||
expect(inst.fills?.[0]?.fillColor).toBe(mainColor);
|
const resetFill = inst.fills?.[0];
|
||||||
|
expect(
|
||||||
|
typeof resetFill === 'string' ? resetFill : resetFill?.fillColor,
|
||||||
|
).toBe(mainColor);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resetOverrides on a plain shape throws', (ctx) => {
|
test('resetOverrides on a plain shape throws', (ctx) => {
|
||||||
@ -266,6 +274,23 @@ describe('Component instances', () => {
|
|||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('swapComponent rejects a component nesting loop', (ctx) => {
|
||||||
|
const leaf = ctx.penpot.createRectangle();
|
||||||
|
ctx.board.appendChild(leaf);
|
||||||
|
const inner = ctx.penpot.library.local.createComponent([leaf]);
|
||||||
|
|
||||||
|
const wrapper = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(wrapper);
|
||||||
|
wrapper.appendChild(inner.instance());
|
||||||
|
const outer = ctx.penpot.library.local.createComponent([wrapper]);
|
||||||
|
const nested = (outer.mainInstance() as Board).children.find((child) =>
|
||||||
|
child.isComponentInstance(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nested).toBeDefined();
|
||||||
|
if (nested) expect(() => nested.swapComponent(outer)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('two instances of one component are independent but share the source', (ctx) => {
|
test('two instances of one component are independent but share the source', (ctx) => {
|
||||||
const comp = makeComponent(ctx);
|
const comp = makeComponent(ctx);
|
||||||
const first = comp.instance();
|
const first = comp.instance();
|
||||||
|
|||||||
@ -51,7 +51,9 @@ describe('File', () => {
|
|||||||
// The exporter service may be unavailable in the headless runner, so a
|
// The exporter service may be unavailable in the headless runner, so a
|
||||||
// rejection here is treated as an environment limitation; when it does
|
// rejection here is treated as an environment limitation; when it does
|
||||||
// run, the result must be a non-empty byte array.
|
// run, the result must be a non-empty byte array.
|
||||||
const data = await file.export('penpot', 'detach').catch(() => null);
|
const data = await file
|
||||||
|
.export('penpot', 'detach-libraries')
|
||||||
|
.catch(() => null);
|
||||||
if (data) {
|
if (data) {
|
||||||
expect(data.length).toBeGreaterThan(0);
|
expect(data.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -267,14 +267,16 @@ describe('Fills & strokes', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('negative strokeWidth is accepted (currently unvalidated)', (ctx) => {
|
test('negative strokeWidth throws', (ctx) => {
|
||||||
// The plugin API does not constrain strokeWidth to be non-negative, so a
|
|
||||||
// negative value is stored as-is rather than rejected. This pins the current
|
|
||||||
// (lenient) behaviour.
|
|
||||||
const r = rect(ctx);
|
const r = rect(ctx);
|
||||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: -3 }];
|
expect(() => {
|
||||||
expect(r.strokes).toHaveLength(1);
|
r.strokes = [{ strokeColor: '#000000', strokeWidth: -3 }];
|
||||||
expect(typeof r.strokes[0].strokeWidth).toBe('number');
|
}).toThrow();
|
||||||
|
|
||||||
|
r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||||
|
expect(() => {
|
||||||
|
r.strokes[0].strokeWidth = -1;
|
||||||
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('invalid strokeStyle throws', (ctx) => {
|
test('invalid strokeStyle throws', (ctx) => {
|
||||||
|
|||||||
@ -119,4 +119,17 @@ describe('Fonts', () => {
|
|||||||
expect(t.fontVariantId).toBe(variant.fontVariantId);
|
expect(t.fontVariantId).toBe(variant.fontVariantId);
|
||||||
expect(t.fontWeight).toBe(variant.fontWeight);
|
expect(t.fontWeight).toBe(variant.fontWeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a font rejects a variant owned by another font', (ctx) => {
|
||||||
|
const fonts = ctx.penpot.fonts.all;
|
||||||
|
const first = fonts[0];
|
||||||
|
const second = fonts.find((font) => font.fontId !== first.fontId);
|
||||||
|
if (second) {
|
||||||
|
const t = text(ctx);
|
||||||
|
expect(() => first.applyToText(t, second.variants[0])).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
first.applyToRange(t.getRange(0, 5), second.variants[0]),
|
||||||
|
).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -156,7 +156,7 @@ describe('Interactions', () => {
|
|||||||
|
|
||||||
test('after-delay trigger carries a delay', (ctx) => {
|
test('after-delay trigger carries a delay', (ctx) => {
|
||||||
const dest = board(ctx);
|
const dest = board(ctx);
|
||||||
const r = rect(ctx);
|
const r = board(ctx);
|
||||||
const interaction = r.addInteraction(
|
const interaction = r.addInteraction(
|
||||||
'after-delay',
|
'after-delay',
|
||||||
{ type: 'navigate-to', destination: dest },
|
{ type: 'navigate-to', destination: dest },
|
||||||
@ -169,7 +169,7 @@ describe('Interactions', () => {
|
|||||||
// A zero delay is a valid value (fires immediately), not an error.
|
// A zero delay is a valid value (fires immediately), not an error.
|
||||||
test('after-delay accepts a zero delay', (ctx) => {
|
test('after-delay accepts a zero delay', (ctx) => {
|
||||||
const dest = board(ctx);
|
const dest = board(ctx);
|
||||||
const r = rect(ctx);
|
const r = board(ctx);
|
||||||
const interaction = r.addInteraction(
|
const interaction = r.addInteraction(
|
||||||
'after-delay',
|
'after-delay',
|
||||||
{ type: 'navigate-to', destination: dest },
|
{ type: 'navigate-to', destination: dest },
|
||||||
@ -198,7 +198,7 @@ describe('Interactions', () => {
|
|||||||
// "don't persist" — that is stale: CI confirms they do.)
|
// "don't persist" — that is stale: CI confirms they do.)
|
||||||
test('interaction delay and action setters persist', (ctx) => {
|
test('interaction delay and action setters persist', (ctx) => {
|
||||||
const dest = board(ctx);
|
const dest = board(ctx);
|
||||||
const r = rect(ctx);
|
const r = board(ctx);
|
||||||
const interaction = r.addInteraction(
|
const interaction = r.addInteraction(
|
||||||
'after-delay',
|
'after-delay',
|
||||||
{ type: 'navigate-to', destination: dest },
|
{ type: 'navigate-to', destination: dest },
|
||||||
@ -218,7 +218,7 @@ describe('Interactions', () => {
|
|||||||
// The delay setter accepts zero (fires immediately) as a valid value.
|
// The delay setter accepts zero (fires immediately) as a valid value.
|
||||||
test('delay setter accepts a zero value', (ctx) => {
|
test('delay setter accepts a zero value', (ctx) => {
|
||||||
const dest = board(ctx);
|
const dest = board(ctx);
|
||||||
const r = rect(ctx);
|
const r = board(ctx);
|
||||||
const interaction = r.addInteraction(
|
const interaction = r.addInteraction(
|
||||||
'after-delay',
|
'after-delay',
|
||||||
{ type: 'navigate-to', destination: dest },
|
{ type: 'navigate-to', destination: dest },
|
||||||
@ -425,35 +425,205 @@ describe('Interactions', () => {
|
|||||||
expect(interaction.trigger).toBe('mouse-enter');
|
expect(interaction.trigger).toBe('mouse-enter');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('unknown interaction triggers are rejected', (ctx) => {
|
||||||
|
const dest = board(ctx);
|
||||||
|
const r = rect(ctx);
|
||||||
|
expect(() =>
|
||||||
|
r.addInteraction('unknown-trigger' as unknown as 'click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: dest,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
|
const interaction = r.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: dest,
|
||||||
|
});
|
||||||
|
expect(() => {
|
||||||
|
interaction.trigger = 'unknown-trigger' as unknown as 'click';
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Edge cases. "fail" tests assert invalid interaction input is
|
// Edge cases. "fail" tests assert invalid interaction input is
|
||||||
// rejected; the "success" test checks several triggers coexisting.
|
// rejected; the "success" test checks several triggers coexisting.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// addInteraction validates the interaction's structure (schema) but not the
|
test('navigate-to a removed board throws', (ctx) => {
|
||||||
// liveness of a navigate destination nor the format of an open-url string,
|
|
||||||
// so both of these are accepted rather than rejected. These pin the current
|
|
||||||
// (lenient) behaviour.
|
|
||||||
test('navigate-to a removed board is accepted (dangling destination)', (ctx) => {
|
|
||||||
const dest = board(ctx);
|
const dest = board(ctx);
|
||||||
const r = rect(ctx);
|
const r = rect(ctx);
|
||||||
dest.remove();
|
dest.remove();
|
||||||
expect(() =>
|
expect(() =>
|
||||||
r.addInteraction('click', { type: 'navigate-to', destination: dest }),
|
r.addInteraction('click', { type: 'navigate-to', destination: dest }),
|
||||||
).not.toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('open-url accepts an arbitrary url string', (ctx) => {
|
test('open-url rejects invalid input and normalizes a bare hostname', (ctx) => {
|
||||||
const r = rect(ctx);
|
const r = rect(ctx);
|
||||||
|
expect(() =>
|
||||||
|
r.addInteraction('click', {
|
||||||
|
type: 'open-url',
|
||||||
|
url: 'not a valid url',
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
r.addInteraction('click', {
|
||||||
|
type: 'open-url',
|
||||||
|
url: 'ftp://example.com/file',
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
const interaction = r.addInteraction('click', {
|
const interaction = r.addInteraction('click', {
|
||||||
type: 'open-url',
|
type: 'open-url',
|
||||||
url: 'not a valid url',
|
url: 'example.com/path',
|
||||||
});
|
});
|
||||||
expect(interaction.action.type).toBe('open-url');
|
expect(interaction.action.type).toBe('open-url');
|
||||||
if (interaction.action.type === 'open-url') {
|
if (interaction.action.type === 'open-url') {
|
||||||
expect(interaction.action.url).toBe('not a valid url');
|
expect(interaction.action.url).toBe('http://example.com/path');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('after-delay is board-only and initializes its default delay', (ctx) => {
|
||||||
|
const dest = board(ctx);
|
||||||
|
const r = rect(ctx);
|
||||||
|
expect(() =>
|
||||||
|
r.addInteraction('after-delay', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: dest,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
|
const source = board(ctx);
|
||||||
|
const interaction = source.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: dest,
|
||||||
|
});
|
||||||
|
interaction.trigger = 'after-delay';
|
||||||
|
expect(interaction.delay).toBeCloseTo(600, 0);
|
||||||
|
expect(() => {
|
||||||
|
const invalid = r.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: dest,
|
||||||
|
});
|
||||||
|
invalid.trigger = 'after-delay';
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('after-delay creation rejects invalid delays', (ctx) => {
|
||||||
|
const dest = board(ctx);
|
||||||
|
const source = board(ctx);
|
||||||
|
for (const delay of ['bad', 1.5, -1]) {
|
||||||
|
expect(() =>
|
||||||
|
source.addInteraction(
|
||||||
|
'after-delay',
|
||||||
|
{ type: 'navigate-to', destination: dest },
|
||||||
|
delay as unknown as number,
|
||||||
|
),
|
||||||
|
).toThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
const interaction = source.addInteraction(
|
||||||
|
'after-delay',
|
||||||
|
{ type: 'navigate-to', destination: dest },
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
for (const delay of ['bad', 1.5, -1]) {
|
||||||
|
expect(() => {
|
||||||
|
interaction.delay = delay as unknown as number;
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigation destinations must be eligible boards', (ctx) => {
|
||||||
|
const source = board(ctx);
|
||||||
|
const child = rect(ctx);
|
||||||
|
const rectangle = rect(ctx);
|
||||||
|
expect(() =>
|
||||||
|
source.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: source,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
child.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: ctx.board,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
source.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: rectangle as unknown as Board,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('interaction destinations must belong to the current page', async (ctx) => {
|
||||||
|
const original = ctx.penpot.currentPage;
|
||||||
|
expect(original).not.toBeNull();
|
||||||
|
if (!original) return;
|
||||||
|
|
||||||
|
const source = rect(ctx);
|
||||||
|
const localDestination = board(ctx);
|
||||||
|
const interaction = source.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: localDestination,
|
||||||
|
});
|
||||||
|
const otherPage = ctx.penpot.createPage();
|
||||||
|
try {
|
||||||
|
await ctx.penpot.openPage(otherPage);
|
||||||
|
const otherBoard = ctx.penpot.createBoard();
|
||||||
|
(otherPage.root as Board).appendChild(otherBoard);
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
source.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: otherBoard,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
interaction.action = {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: otherBoard,
|
||||||
|
};
|
||||||
|
}).toThrow();
|
||||||
|
} finally {
|
||||||
|
if (ctx.penpot.currentPage?.id !== original.id) {
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
}
|
||||||
|
otherPage.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('push animation is rejected for overlay actions and replacements', (ctx) => {
|
||||||
|
const overlay = board(ctx);
|
||||||
|
const r = rect(ctx);
|
||||||
|
const push = {
|
||||||
|
type: 'push' as const,
|
||||||
|
direction: 'left' as const,
|
||||||
|
duration: 300,
|
||||||
|
easing: 'linear' as const,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
r.addInteraction('click', {
|
||||||
|
type: 'open-overlay',
|
||||||
|
destination: overlay,
|
||||||
|
animation: push,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
|
const interaction = r.addInteraction('click', {
|
||||||
|
type: 'navigate-to',
|
||||||
|
destination: overlay,
|
||||||
|
});
|
||||||
|
expect(() => {
|
||||||
|
interaction.action = {
|
||||||
|
type: 'open-overlay',
|
||||||
|
destination: overlay,
|
||||||
|
animation: push,
|
||||||
|
};
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('several triggers on one shape coexist', (ctx) => {
|
test('several triggers on one shape coexist', (ctx) => {
|
||||||
const dest = board(ctx);
|
const dest = board(ctx);
|
||||||
const r = rect(ctx);
|
const r = rect(ctx);
|
||||||
|
|||||||
@ -82,6 +82,34 @@ describe('Layout', () => {
|
|||||||
expect(flex.leftPadding).toBeCloseTo(4.5, 2);
|
expect(flex.leftPadding).toBeCloseTo(4.5, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('every flex gap and padding setter rejects negative values', (ctx) => {
|
||||||
|
const flex = board(ctx).addFlexLayout();
|
||||||
|
expect(() => {
|
||||||
|
flex.rowGap = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.columnGap = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.verticalPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.horizontalPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.topPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.rightPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.bottomPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flex.leftPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
// paddingType is "simple" (sides mirrored) or "multiple" (each side independent).
|
// paddingType is "simple" (sides mirrored) or "multiple" (each side independent).
|
||||||
test('paddingType round-trips', (ctx) => {
|
test('paddingType round-trips', (ctx) => {
|
||||||
const flex = board(ctx).addFlexLayout();
|
const flex = board(ctx).addFlexLayout();
|
||||||
@ -185,6 +213,37 @@ describe('Layout', () => {
|
|||||||
expect(grid.columns[0].type).toBe('percent');
|
expect(grid.columns[0].type).toBe('percent');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('grid track creation and replacement reject negative values', (ctx) => {
|
||||||
|
const grid = board(ctx).addGridLayout();
|
||||||
|
for (const type of ['fixed', 'percent', 'flex'] as const) {
|
||||||
|
expect(() => grid.addRow(type, -1)).toThrow();
|
||||||
|
expect(() => grid.addColumn(type, -1)).toThrow();
|
||||||
|
}
|
||||||
|
expect(() =>
|
||||||
|
grid.addColumn('fixed', 'bad' as unknown as number),
|
||||||
|
).toThrow();
|
||||||
|
grid.addRow('flex', 1);
|
||||||
|
grid.addColumn('flex', 1);
|
||||||
|
expect(() => grid.addRowAtIndex(0, 'fixed', -1)).toThrow();
|
||||||
|
expect(() => grid.addColumnAtIndex(0, 'fixed', -1)).toThrow();
|
||||||
|
expect(() => grid.setRow(0, 'flex', -1)).toThrow();
|
||||||
|
expect(() => grid.setColumn(0, 'flex', -1)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retained grid track proxies reject negative values', (ctx) => {
|
||||||
|
const grid = board(ctx).addGridLayout();
|
||||||
|
grid.addRow('fixed', 10);
|
||||||
|
grid.addColumn('fixed', 10);
|
||||||
|
const row = grid.rows[0];
|
||||||
|
const column = grid.columns[0];
|
||||||
|
expect(() => {
|
||||||
|
row.value = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
column.value = -1;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('removeRow and removeColumn drop tracks', (ctx) => {
|
test('removeRow and removeColumn drop tracks', (ctx) => {
|
||||||
const grid = board(ctx).addGridLayout();
|
const grid = board(ctx).addGridLayout();
|
||||||
grid.addRow('flex', 1);
|
grid.addRow('flex', 1);
|
||||||
@ -238,6 +297,34 @@ describe('Layout', () => {
|
|||||||
expect(grid.leftPadding).toBeCloseTo(4.5, 2);
|
expect(grid.leftPadding).toBeCloseTo(4.5, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('every grid gap and padding setter rejects negative values', (ctx) => {
|
||||||
|
const grid = board(ctx).addGridLayout();
|
||||||
|
expect(() => {
|
||||||
|
grid.rowGap = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.columnGap = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.verticalPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.horizontalPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.topPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.rightPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.bottomPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
grid.leftPadding = -1;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
// paddingType behaves the same as on flex layouts (see issue #10278).
|
// paddingType behaves the same as on flex layouts (see issue #10278).
|
||||||
test('paddingType round-trips', (ctx) => {
|
test('paddingType round-trips', (ctx) => {
|
||||||
const grid = board(ctx).addGridLayout();
|
const grid = board(ctx).addGridLayout();
|
||||||
@ -433,6 +520,29 @@ describe('Layout', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('every layout child min and max bound rejects negatives', (ctx) => {
|
||||||
|
const b = board(ctx);
|
||||||
|
const flex = b.addFlexLayout();
|
||||||
|
const rect = ctx.penpot.createRectangle();
|
||||||
|
flex.appendChild(rect);
|
||||||
|
const child = rect.layoutChild;
|
||||||
|
expect(child).toBeDefined();
|
||||||
|
if (child) {
|
||||||
|
expect(() => {
|
||||||
|
child.minWidth = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
child.maxWidth = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
child.minHeight = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
child.maxHeight = -1;
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// marginType is the child-margin counterpart of a layout's paddingType.
|
// marginType is the child-margin counterpart of a layout's paddingType.
|
||||||
test('marginType round-trips', (ctx) => {
|
test('marginType round-trips', (ctx) => {
|
||||||
const b = board(ctx);
|
const b = board(ctx);
|
||||||
@ -491,7 +601,7 @@ describe('Layout', () => {
|
|||||||
b.resize(300, 200);
|
b.resize(300, 200);
|
||||||
b.addFlexLayout();
|
b.addFlexLayout();
|
||||||
|
|
||||||
const found = ctx.penpot.currentPage.getShapeById(b.id) as Board;
|
const found = ctx.penpot.currentPage!.getShapeById(b.id) as Board;
|
||||||
expect(found).not.toBeNull();
|
expect(found).not.toBeNull();
|
||||||
const child = ctx.penpot.createRectangle();
|
const child = ctx.penpot.createRectangle();
|
||||||
found.appendChild(child);
|
found.appendChild(child);
|
||||||
|
|||||||
@ -206,28 +206,85 @@ describe('Library', () => {
|
|||||||
const typo = ctx.penpot.library.local.createTypography();
|
const typo = ctx.penpot.library.local.createTypography();
|
||||||
expect(typeof typo.fontFamily).toBe('string');
|
expect(typeof typo.fontFamily).toBe('string');
|
||||||
|
|
||||||
typo.fontFamily = 'Arial';
|
const font = ctx.penpot.fonts.all[0];
|
||||||
typo.fontId = 'gfont-arial';
|
typo.fontFamily = font.fontFamily;
|
||||||
expect(typo.fontFamily).toBe('Arial');
|
typo.fontId = font.fontId;
|
||||||
expect(typo.fontId).toBe('gfont-arial');
|
expect(typo.fontFamily).toBe(font.fontFamily);
|
||||||
|
expect(typo.fontId).toBe(font.fontId);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('typography style members round-trip', (ctx) => {
|
test('typography style members round-trip', (ctx) => {
|
||||||
const typo = ctx.penpot.library.local.createTypography();
|
const typo = ctx.penpot.library.local.createTypography();
|
||||||
typo.fontStyle = 'italic';
|
const font =
|
||||||
|
ctx.penpot.fonts.all.find((item) =>
|
||||||
|
item.variants.some((variant) => variant.fontStyle === 'italic'),
|
||||||
|
) ?? ctx.penpot.fonts.all[0];
|
||||||
|
typo.setFont(font);
|
||||||
|
const italic = font.variants.find(
|
||||||
|
(variant) => variant.fontStyle === 'italic',
|
||||||
|
);
|
||||||
|
if (italic) {
|
||||||
|
typo.fontStyle = italic.fontStyle;
|
||||||
|
expect(typo.fontStyle).toBe(italic.fontStyle);
|
||||||
|
expect(typo.fontVariantId).toBe(italic.fontVariantId);
|
||||||
|
expect(typo.fontWeight).toBe(italic.fontWeight);
|
||||||
|
}
|
||||||
typo.textTransform = 'uppercase';
|
typo.textTransform = 'uppercase';
|
||||||
typo.fontWeight = '700';
|
const variant = font.variants[font.variants.length - 1];
|
||||||
typo.fontVariantId = 'regular';
|
typo.fontWeight = variant.fontWeight;
|
||||||
|
typo.fontVariantId = variant.fontVariantId;
|
||||||
typo.lineHeight = '1.5';
|
typo.lineHeight = '1.5';
|
||||||
typo.letterSpacing = '1';
|
typo.letterSpacing = '1';
|
||||||
expect(typo.fontStyle).toBe('italic');
|
|
||||||
expect(typo.textTransform).toBe('uppercase');
|
expect(typo.textTransform).toBe('uppercase');
|
||||||
expect(typo.fontWeight).toBe('700');
|
expect(typo.fontWeight).toBe(variant.fontWeight);
|
||||||
expect(typo.fontVariantId).toBe('regular');
|
expect(typo.fontStyle).toBe(variant.fontStyle);
|
||||||
expect(typeof typo.lineHeight).toBe('string');
|
expect(typo.fontVariantId).toBe(variant.fontVariantId);
|
||||||
|
expect(typo.lineHeight).toBe('1.5');
|
||||||
expect(typeof typo.letterSpacing).toBe('string');
|
expect(typeof typo.letterSpacing).toBe('string');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('typography text values use text validation', (ctx) => {
|
||||||
|
const typo = ctx.penpot.library.local.createTypography();
|
||||||
|
expect(() => {
|
||||||
|
typo.fontSize = '2';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.fontSize = '12px';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.lineHeight = '201';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.lineHeight = '12px';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.letterSpacing = '12px';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.textTransform = 'not-a-transform' as unknown as 'uppercase';
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typography font fields require installed fonts and variants', (ctx) => {
|
||||||
|
const typo = ctx.penpot.library.local.createTypography();
|
||||||
|
expect(() => {
|
||||||
|
typo.fontId = 'missing-font';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.fontFamily = 'Missing Font Family';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typo.fontVariantId = 'missing-variant';
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
const fonts = ctx.penpot.fonts.all;
|
||||||
|
const first = fonts[0];
|
||||||
|
const second = fonts.find((font) => font.fontId !== first.fontId);
|
||||||
|
if (second) {
|
||||||
|
expect(() => typo.setFont(first, second.variants[0])).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('typography setFont updates the font', (ctx) => {
|
test('typography setFont updates the font', (ctx) => {
|
||||||
const typo = ctx.penpot.library.local.createTypography();
|
const typo = ctx.penpot.library.local.createTypography();
|
||||||
const font = ctx.penpot.fonts.all[0];
|
const font = ctx.penpot.fonts.all[0];
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { expect } from '../framework/expect';
|
import { expect } from '../framework/expect';
|
||||||
import { describe, test } from '../framework/registry';
|
import { describe, test } from '../framework/registry';
|
||||||
|
import type { Board } from '@penpot/plugin-types';
|
||||||
|
|
||||||
// Pages, selection and flows.
|
// Pages, selection and flows.
|
||||||
// Most assertions use the active page (`currentPage`) and the scratch board so
|
// Most assertions use the active page (`currentPage`) and the scratch board so
|
||||||
@ -16,6 +17,18 @@ describe('Pages', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('page names are trimmed and cannot be blank', (ctx) => {
|
||||||
|
const page = ctx.penpot.currentPage;
|
||||||
|
expect(page).not.toBeNull();
|
||||||
|
if (page) {
|
||||||
|
expect(() => {
|
||||||
|
page.name = ' ';
|
||||||
|
}).toThrow();
|
||||||
|
page.name = ' Trimmed page ';
|
||||||
|
expect(page.name).toBe('Trimmed page');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('createPage and openPage activate a new page', async (ctx) => {
|
test('createPage and openPage activate a new page', async (ctx) => {
|
||||||
const original = ctx.penpot.currentPage;
|
const original = ctx.penpot.currentPage;
|
||||||
const page = ctx.penpot.createPage();
|
const page = ctx.penpot.createPage();
|
||||||
@ -206,4 +219,66 @@ describe('Flows', () => {
|
|||||||
expect(page.flows.length).toBe(before - 1);
|
expect(page.flows.length).toBe(before - 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('flows require a live, unused board on their page', (ctx) => {
|
||||||
|
const page = ctx.penpot.currentPage;
|
||||||
|
expect(page).not.toBeNull();
|
||||||
|
if (page) {
|
||||||
|
const rect = ctx.penpot.createRectangle();
|
||||||
|
ctx.board.appendChild(rect);
|
||||||
|
expect(() =>
|
||||||
|
page.createFlow('rect-flow', rect as unknown as Board),
|
||||||
|
).toThrow();
|
||||||
|
|
||||||
|
const removed = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(removed);
|
||||||
|
removed.remove();
|
||||||
|
expect(() => page.createFlow('removed-flow', removed)).toThrow();
|
||||||
|
|
||||||
|
const target = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(target);
|
||||||
|
const first = page.createFlow('first-flow', target);
|
||||||
|
expect(() => page.createFlow('duplicate-flow', target)).toThrow();
|
||||||
|
|
||||||
|
const secondTarget = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(secondTarget);
|
||||||
|
const second = page.createFlow('second-flow', secondTarget);
|
||||||
|
expect(() => {
|
||||||
|
second.startingBoard = target;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
second.startingBoard = removed;
|
||||||
|
}).toThrow();
|
||||||
|
first.remove();
|
||||||
|
second.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flows reject a starting board from another page', async (ctx) => {
|
||||||
|
const original = ctx.penpot.currentPage;
|
||||||
|
expect(original).not.toBeNull();
|
||||||
|
if (!original) return;
|
||||||
|
|
||||||
|
const localBoard = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(localBoard);
|
||||||
|
const flow = original.createFlow('cross-page-flow', localBoard);
|
||||||
|
const otherPage = ctx.penpot.createPage();
|
||||||
|
try {
|
||||||
|
await ctx.penpot.openPage(otherPage);
|
||||||
|
const otherBoard = ctx.penpot.createBoard();
|
||||||
|
(otherPage.root as Board).appendChild(otherBoard);
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
|
||||||
|
expect(() => original.createFlow('foreign-flow', otherBoard)).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
flow.startingBoard = otherBoard;
|
||||||
|
}).toThrow();
|
||||||
|
} finally {
|
||||||
|
if (ctx.penpot.currentPage?.id !== original.id) {
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
}
|
||||||
|
flow.remove();
|
||||||
|
otherPage.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -58,6 +58,36 @@ describe('Shadows', () => {
|
|||||||
expect(shadow.style).toBe('inner-shadow');
|
expect(shadow.style).toBe('inner-shadow');
|
||||||
expect(shadow.hidden).toBe(true);
|
expect(shadow.hidden).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('negative shadow blur throws', (ctx) => {
|
||||||
|
const r = rect(ctx);
|
||||||
|
expect(() => {
|
||||||
|
r.shadows = [
|
||||||
|
{
|
||||||
|
style: 'drop-shadow',
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
blur: -1,
|
||||||
|
spread: 0,
|
||||||
|
color: { color: '#000000', opacity: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
r.shadows = [
|
||||||
|
{
|
||||||
|
style: 'drop-shadow',
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
blur: 1,
|
||||||
|
spread: 0,
|
||||||
|
color: { color: '#000000', opacity: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expect(() => {
|
||||||
|
r.shadows[0].blur = -1;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Blur', () => {
|
describe('Blur', () => {
|
||||||
@ -78,4 +108,14 @@ describe('Blur', () => {
|
|||||||
expect(r.backgroundBlur).toBeDefined();
|
expect(r.backgroundBlur).toBeDefined();
|
||||||
expect(r.backgroundBlur && r.backgroundBlur.value).toBeCloseTo(5, 0);
|
expect(r.backgroundBlur && r.backgroundBlur.value).toBeCloseTo(5, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('negative layer and background blur throw', (ctx) => {
|
||||||
|
const r = rect(ctx);
|
||||||
|
expect(() => {
|
||||||
|
r.blur = { value: -1 };
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
r.backgroundBlur = { value: -1 };
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { expect } from '../framework/expect';
|
import { expect } from '../framework/expect';
|
||||||
import { describe, test } from '../framework/registry';
|
import { describe, test } from '../framework/registry';
|
||||||
|
import type { Group } from '@penpot/plugin-types';
|
||||||
import type { TestContext } from '../framework/types';
|
import type { TestContext } from '../framework/types';
|
||||||
|
|
||||||
// Shapes & geometry.
|
// Shapes & geometry.
|
||||||
@ -254,6 +255,22 @@ describe('Shapes', () => {
|
|||||||
expect(r.borderRadiusBottomRight).toBeCloseTo(3.75, 2);
|
expect(r.borderRadiusBottomRight).toBeCloseTo(3.75, 2);
|
||||||
expect(r.borderRadiusBottomLeft).toBeCloseTo(0.5, 2);
|
expect(r.borderRadiusBottomLeft).toBeCloseTo(0.5, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('individual corner radii reject negative values', (ctx) => {
|
||||||
|
const r = rect(ctx);
|
||||||
|
expect(() => {
|
||||||
|
r.borderRadiusTopLeft = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
r.borderRadiusTopRight = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
r.borderRadiusBottomRight = -1;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
r.borderRadiusBottomLeft = -1;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Ordering', () => {
|
describe('Ordering', () => {
|
||||||
@ -365,7 +382,7 @@ describe('Shapes', () => {
|
|||||||
expect(group).not.toBeNull();
|
expect(group).not.toBeNull();
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
|
|
||||||
const copy = group.clone();
|
const copy = group.clone() as Group;
|
||||||
ctx.board.appendChild(copy);
|
ctx.board.appendChild(copy);
|
||||||
expect(copy.id).not.toBe(group.id);
|
expect(copy.id).not.toBe(group.id);
|
||||||
expect(copy.children).toHaveLength(group.children.length);
|
expect(copy.children).toHaveLength(group.children.length);
|
||||||
@ -447,6 +464,21 @@ describe('Shapes', () => {
|
|||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('export scale must be positive', (ctx) => {
|
||||||
|
const r = rect(ctx);
|
||||||
|
expect(() => {
|
||||||
|
r.exports = [{ type: 'png', scale: 0, suffix: '' }];
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
r.exports = [{ type: 'png', scale: -1, suffix: '' }];
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
r.exports = [{ type: 'png', scale: 1, suffix: '' }];
|
||||||
|
expect(() => {
|
||||||
|
r.exports[0].scale = 0;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('resize to zero dimensions throws', (ctx) => {
|
test('resize to zero dimensions throws', (ctx) => {
|
||||||
const r = rect(ctx);
|
const r = rect(ctx);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
|||||||
@ -302,6 +302,60 @@ describe('Text', () => {
|
|||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('text numeric fields require complete numbers within editor bounds', (ctx) => {
|
||||||
|
const t = text(ctx);
|
||||||
|
for (const value of ['.', '-', '12px', '2', '1001']) {
|
||||||
|
expect(() => {
|
||||||
|
t.fontSize = value;
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
for (const value of ['.', '-', '12px', '-201', '201']) {
|
||||||
|
expect(() => {
|
||||||
|
t.lineHeight = value;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
t.letterSpacing = value;
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('text range numeric fields use the same validation', (ctx) => {
|
||||||
|
const range = text(ctx, 'Hello').getRange(0, 5);
|
||||||
|
expect(() => {
|
||||||
|
range.fontSize = '12px';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
range.lineHeight = '12px';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
range.letterSpacing = '12px';
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('text and range font fields reject missing fonts and variants', (ctx) => {
|
||||||
|
const t = text(ctx, 'Hello');
|
||||||
|
expect(() => {
|
||||||
|
t.fontId = 'missing-font';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
t.fontFamily = 'Missing Font Family';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
t.fontVariantId = 'missing-variant';
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
const range = t.getRange(0, 5);
|
||||||
|
expect(() => {
|
||||||
|
range.fontId = 'missing-font';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
range.fontFamily = 'Missing Font Family';
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
range.fontVariantId = 'missing-variant';
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('invalid align value throws', (ctx) => {
|
test('invalid align value throws', (ctx) => {
|
||||||
const t = text(ctx);
|
const t = text(ctx);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@ -314,6 +368,9 @@ describe('Text', () => {
|
|||||||
expect(() => {
|
expect(() => {
|
||||||
t.textTransform = 'UPPERCASE' as unknown as 'uppercase';
|
t.textTransform = 'UPPERCASE' as unknown as 'uppercase';
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
t.getRange(0, 1).textTransform = 'UPPERCASE' as unknown as 'uppercase';
|
||||||
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('invalid direction value throws', (ctx) => {
|
test('invalid direction value throws', (ctx) => {
|
||||||
|
|||||||
@ -56,6 +56,20 @@ describe('Tokens', () => {
|
|||||||
expect(cat.themes.length).toBeGreaterThan(0);
|
expect(cat.themes.length).toBeGreaterThan(0);
|
||||||
expect(cat.getThemeById(theme.id)).toBeDefined();
|
expect(cat.getThemeById(theme.id)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('catalog name validation uses current state and rejects blank names', (ctx) => {
|
||||||
|
const cat = catalog(ctx);
|
||||||
|
const name = unique('live-set');
|
||||||
|
cat.addSet({ name });
|
||||||
|
expect(() => cat.addSet({ name })).toThrow();
|
||||||
|
expect(() => cat.addSet({ name: ' ' })).toThrow();
|
||||||
|
expect(() => cat.addTheme({ group: '', name: ' ' })).toThrow();
|
||||||
|
|
||||||
|
const group = unique('live-theme-group');
|
||||||
|
const themeName = unique('live-theme');
|
||||||
|
cat.addTheme({ group, name: themeName });
|
||||||
|
expect(() => cat.addTheme({ group, name: themeName })).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Set', () => {
|
describe('Set', () => {
|
||||||
@ -70,6 +84,17 @@ describe('Tokens', () => {
|
|||||||
expect(set.active).toBe(true);
|
expect(set.active).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('retained set proxies validate names against current state', (ctx) => {
|
||||||
|
const first = activeSet(ctx, unique('set'));
|
||||||
|
const second = activeSet(ctx, unique('set'));
|
||||||
|
expect(() => {
|
||||||
|
first.name = second.name;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
first.name = ' ';
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
// Community report (forum #10700, issue #14): toggling a token set's
|
// Community report (forum #10700, issue #14): toggling a token set's
|
||||||
// active state was said to freeze and roll back when tokens from the set
|
// active state was said to freeze and roll back when tokens from the set
|
||||||
// are bound to shapes. Did not reproduce; kept as a regression pin.
|
// are bound to shapes. Did not reproduce; kept as a regression pin.
|
||||||
@ -148,15 +173,42 @@ describe('Tokens', () => {
|
|||||||
}),
|
}),
|
||||||
).toThrow();
|
).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('empty composites and line-height without font-size throw', (ctx) => {
|
||||||
|
const set = activeSet(ctx, unique('set'));
|
||||||
|
expect(() =>
|
||||||
|
set.addToken({
|
||||||
|
type: 'typography',
|
||||||
|
name: unique('empty-typography.'),
|
||||||
|
value: {} as never,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
set.addToken({
|
||||||
|
type: 'shadow',
|
||||||
|
name: unique('empty-shadow.'),
|
||||||
|
value: [] as never,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
set.addToken({
|
||||||
|
type: 'typography',
|
||||||
|
name: unique('line-height-only.'),
|
||||||
|
value: { lineHeight: '1.2' } as never,
|
||||||
|
}),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Theme', () => {
|
describe('Theme', () => {
|
||||||
test('group, name and active round-trip', (ctx) => {
|
test('group, name and active round-trip', (ctx) => {
|
||||||
const theme = catalog(ctx).addTheme({ group: '', name: unique('theme') });
|
const theme = catalog(ctx).addTheme({ group: '', name: unique('theme') });
|
||||||
theme.group = 'brand';
|
const group = unique('brand');
|
||||||
theme.name = 'dark';
|
const name = unique('dark');
|
||||||
expect(theme.group).toBe('brand');
|
theme.group = group;
|
||||||
expect(theme.name).toBe('dark');
|
theme.name = name;
|
||||||
|
expect(theme.group).toBe(group);
|
||||||
|
expect(theme.name).toBe(name);
|
||||||
theme.active = true;
|
theme.active = true;
|
||||||
expect(theme.active).toBe(true);
|
expect(theme.active).toBe(true);
|
||||||
theme.toggleActive();
|
theme.toggleActive();
|
||||||
@ -191,6 +243,33 @@ describe('Tokens', () => {
|
|||||||
expect(dup.id).not.toBe(theme.id);
|
expect(dup.id).not.toBe(theme.id);
|
||||||
dup.remove();
|
dup.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('theme names stay unique within their current group', (ctx) => {
|
||||||
|
const cat = catalog(ctx);
|
||||||
|
const group = unique('group');
|
||||||
|
const first = cat.addTheme({ group, name: unique('theme') });
|
||||||
|
const second = cat.addTheme({ group, name: unique('theme') });
|
||||||
|
expect(() => {
|
||||||
|
first.name = second.name;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
first.name = ' ';
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
const duplicate = first.duplicate();
|
||||||
|
expect(duplicate.name).not.toBe(first.name);
|
||||||
|
duplicate.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('moving a retained theme cannot create a group/name collision', (ctx) => {
|
||||||
|
const cat = catalog(ctx);
|
||||||
|
const name = unique('shared-theme');
|
||||||
|
const first = cat.addTheme({ group: unique('group-a'), name });
|
||||||
|
const second = cat.addTheme({ group: unique('group-b'), name });
|
||||||
|
expect(() => {
|
||||||
|
first.group = second.group;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Token', () => {
|
describe('Token', () => {
|
||||||
@ -315,9 +394,131 @@ describe('Tokens', () => {
|
|||||||
});
|
});
|
||||||
const dup = token.duplicate();
|
const dup = token.duplicate();
|
||||||
expect(dup.id).not.toBe(token.id);
|
expect(dup.id).not.toBe(token.id);
|
||||||
|
expect(dup.name).not.toBe(token.name);
|
||||||
dup.remove();
|
dup.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('token edits reject missing, self, cyclic, and dotted-name conflicts', (ctx) => {
|
||||||
|
const set = activeSet(ctx, unique('set'));
|
||||||
|
const prefix = unique('tree');
|
||||||
|
set.addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: `${prefix}.child`,
|
||||||
|
value: '8',
|
||||||
|
});
|
||||||
|
const token = set.addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: unique('editable.'),
|
||||||
|
value: '4',
|
||||||
|
});
|
||||||
|
expect(() => {
|
||||||
|
token.name = prefix;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
token.value = `{${token.name}}`;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
token.value = '{missing-token}';
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
const other = set.addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: unique('other.'),
|
||||||
|
value: '2',
|
||||||
|
});
|
||||||
|
token.value = `{${other.name}}`;
|
||||||
|
expect(() => {
|
||||||
|
other.value = `{${token.name}}`;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retained token proxies validate names against current state', (ctx) => {
|
||||||
|
const set = activeSet(ctx, unique('set'));
|
||||||
|
const retained = set.addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: unique('retained.'),
|
||||||
|
value: '1',
|
||||||
|
});
|
||||||
|
const later = set.addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: unique('later.'),
|
||||||
|
value: '2',
|
||||||
|
});
|
||||||
|
expect(() => {
|
||||||
|
retained.name = later.name;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renaming validates the whole token and rejects a new self-reference', (ctx) => {
|
||||||
|
const targetName = unique('rename-target');
|
||||||
|
activeSet(ctx, unique('target-set')).addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: targetName,
|
||||||
|
value: '8',
|
||||||
|
});
|
||||||
|
const token = activeSet(ctx, unique('source-set')).addToken({
|
||||||
|
type: 'dimension',
|
||||||
|
name: unique('rename-source'),
|
||||||
|
value: `{${targetName}}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
token.name = targetName;
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('composite token updates reject empty values and missing font size', (ctx) => {
|
||||||
|
const set = activeSet(ctx, unique('set'));
|
||||||
|
const typography = set.addToken({
|
||||||
|
type: 'typography',
|
||||||
|
name: unique('typography.'),
|
||||||
|
value: { fontSizes: '14', lineHeight: '1.2' } as never,
|
||||||
|
}) as TokenTypography;
|
||||||
|
const shadow = set.addToken({
|
||||||
|
type: 'shadow',
|
||||||
|
name: unique('shadow.'),
|
||||||
|
value: {
|
||||||
|
color: '#000000',
|
||||||
|
inset: 'false',
|
||||||
|
offsetX: '0',
|
||||||
|
offsetY: '0',
|
||||||
|
spread: '0',
|
||||||
|
blur: '1',
|
||||||
|
},
|
||||||
|
}) as TokenShadow;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
typography.value = {} as never;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
typography.value = { lineHeight: '1.2' } as never;
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
shadow.value = [];
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tokens with newly broken references cannot be applied', (ctx) => {
|
||||||
|
const set = activeSet(ctx, unique('set'));
|
||||||
|
const base = set.addToken({
|
||||||
|
type: 'borderRadius',
|
||||||
|
name: unique('base.'),
|
||||||
|
value: '8',
|
||||||
|
});
|
||||||
|
const ref = set.addToken({
|
||||||
|
type: 'borderRadius',
|
||||||
|
name: unique('ref.'),
|
||||||
|
value: `{${base.name}}`,
|
||||||
|
});
|
||||||
|
base.remove();
|
||||||
|
const rect = ctx.penpot.createRectangle();
|
||||||
|
ctx.board.appendChild(rect);
|
||||||
|
ctx.penpot.selection = [rect];
|
||||||
|
expect(() => ref.applyToShapes([rect])).toThrow();
|
||||||
|
expect(() => ref.applyToSelected()).toThrow();
|
||||||
|
expect(() => rect.applyToken(ref)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
// Reference resolution — a token referencing another resolves transitively.
|
// Reference resolution — a token referencing another resolves transitively.
|
||||||
test('a token referencing another token resolves transitively', (ctx) => {
|
test('a token referencing another token resolves transitively', (ctx) => {
|
||||||
const set = activeSet(ctx, unique('set'));
|
const set = activeSet(ctx, unique('set'));
|
||||||
|
|||||||
@ -247,13 +247,11 @@ describe('Value objects', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('negative blur value is accepted (currently unvalidated)', (ctx) => {
|
test('negative blur value throws', (ctx) => {
|
||||||
// The blur setter does not reject a negative value; this pins the current
|
|
||||||
// lenient behaviour (a candidate for future hardening).
|
|
||||||
const r = rect(ctx);
|
const r = rect(ctx);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
r.blur = { value: -5 };
|
r.blur = { value: -5 };
|
||||||
}).not.toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -232,9 +232,12 @@ describe('Variants', () => {
|
|||||||
|
|
||||||
const instance = vc.instance();
|
const instance = vc.instance();
|
||||||
ctx.board.appendChild(instance);
|
ctx.board.appendChild(instance);
|
||||||
// Valid args (nat-int pos, string value): switches to the nearest variant
|
const property = vc.variants!.properties[0];
|
||||||
// with that value at the property position, or no-ops — never throws.
|
const target =
|
||||||
expect(() => instance.switchVariant(0, 'large')).not.toThrow();
|
vc.variants!.variantComponents()[1] as LibraryVariantComponent;
|
||||||
|
expect(() =>
|
||||||
|
instance.switchVariant(0, target.variantProps[property]),
|
||||||
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Community report (forum #10700, issue #3): switchVariant on an instance
|
// Community report (forum #10700, issue #3): switchVariant on an instance
|
||||||
@ -256,7 +259,12 @@ describe('Variants', () => {
|
|||||||
const clonedInstance = cloned.children.find((s) => s.isComponentInstance());
|
const clonedInstance = cloned.children.find((s) => s.isComponentInstance());
|
||||||
expect(clonedInstance).toBeDefined();
|
expect(clonedInstance).toBeDefined();
|
||||||
if (clonedInstance) {
|
if (clonedInstance) {
|
||||||
expect(() => clonedInstance.switchVariant(0, 'large')).not.toThrow();
|
const property = vc.variants!.properties[0];
|
||||||
|
const target =
|
||||||
|
vc.variants!.variantComponents()[1] as LibraryVariantComponent;
|
||||||
|
expect(() =>
|
||||||
|
clonedInstance.switchVariant(0, target.variantProps[property]),
|
||||||
|
).not.toThrow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -332,6 +340,14 @@ describe('Variants', () => {
|
|||||||
expect(() => ctx.penpot.createVariantFromComponents([])).toThrow();
|
expect(() => ctx.penpot.createVariantFromComponents([])).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createVariantFromComponents requires two distinct components', (ctx) => {
|
||||||
|
const main = componentMain(ctx);
|
||||||
|
expect(() => ctx.penpot.createVariantFromComponents([main])).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
ctx.penpot.createVariantFromComponents([main, main]),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('removeProperty out of bounds throws', async (ctx) => {
|
test('removeProperty out of bounds throws', async (ctx) => {
|
||||||
const vc = await variantComponent(ctx);
|
const vc = await variantComponent(ctx);
|
||||||
const v = vc.variants;
|
const v = vc.variants;
|
||||||
@ -341,6 +357,13 @@ describe('Variants', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the last variant property cannot be removed', async (ctx) => {
|
||||||
|
const vc = await variantComponent(ctx);
|
||||||
|
const v = vc.variants!;
|
||||||
|
expect(v.properties).toHaveLength(1);
|
||||||
|
expect(() => v.removeProperty(0)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
test('renameProperty out of bounds throws', async (ctx) => {
|
test('renameProperty out of bounds throws', async (ctx) => {
|
||||||
const vc = await variantComponent(ctx);
|
const vc = await variantComponent(ctx);
|
||||||
const v = vc.variants;
|
const v = vc.variants;
|
||||||
@ -350,8 +373,88 @@ describe('Variants', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('variant property names are trimmed, nonblank, and at most 60 characters', async (ctx) => {
|
||||||
|
const vc = await variantComponent(ctx);
|
||||||
|
const v = vc.variants!;
|
||||||
|
expect(() => v.renameProperty(0, ' ')).toThrow();
|
||||||
|
expect(() => v.renameProperty(0, 'x'.repeat(61))).toThrow();
|
||||||
|
v.renameProperty(0, ' Size ');
|
||||||
|
expect(v.properties[0]).toBe('Size');
|
||||||
|
});
|
||||||
|
|
||||||
test('setVariantProperty out of bounds throws', async (ctx) => {
|
test('setVariantProperty out of bounds throws', async (ctx) => {
|
||||||
const vc = await variantComponent(ctx);
|
const vc = await variantComponent(ctx);
|
||||||
expect(() => vc.setVariantProperty(999, 'large')).toThrow();
|
expect(() => vc.setVariantProperty(999, 'large')).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('variant values are trimmed and at most 60 characters', async (ctx) => {
|
||||||
|
const vc = await variantComponent(ctx);
|
||||||
|
expect(() => vc.setVariantProperty(0, 'x'.repeat(61))).toThrow();
|
||||||
|
vc.setVariantProperty(0, ' Large ');
|
||||||
|
expect(vc.variantProps[vc.variants!.properties[0]]).toBe('Large');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty variant values remain allowed after trimming', async (ctx) => {
|
||||||
|
const vc = await variantComponent(ctx);
|
||||||
|
expect(() => vc.setVariantProperty(0, ' ')).not.toThrow();
|
||||||
|
expect(vc.variantProps[vc.variants!.properties[0]]).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('switchVariant rejects mains, bad positions, and unavailable values', async (ctx) => {
|
||||||
|
const vc = await variantComponent(ctx);
|
||||||
|
vc.addVariant();
|
||||||
|
await waitFor(() => (vc.variants?.variantComponents().length ?? 0) > 1);
|
||||||
|
const instance = vc.instance();
|
||||||
|
ctx.board.appendChild(instance);
|
||||||
|
expect(() => vc.mainInstance().switchVariant(0, 'Value2')).toThrow();
|
||||||
|
expect(() => instance.switchVariant(999, 'Value2')).toThrow();
|
||||||
|
expect(() => instance.switchVariant(0, 'missing-value')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant combining rejects copies and existing variants', async (ctx) => {
|
||||||
|
const standard = componentWithMain(ctx);
|
||||||
|
const other = componentWithMain(ctx);
|
||||||
|
const copy = other.comp.instance() as Board;
|
||||||
|
ctx.board.appendChild(copy);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
ctx.penpot.createVariantFromComponents([standard.main, copy]),
|
||||||
|
).toThrow();
|
||||||
|
expect(() => standard.main.combineAsVariants([copy.id])).toThrow();
|
||||||
|
|
||||||
|
const variant = await variantComponent(ctx);
|
||||||
|
expect(() =>
|
||||||
|
ctx.penpot.createVariantFromComponents([
|
||||||
|
standard.main,
|
||||||
|
variant.mainInstance() as Board,
|
||||||
|
]),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
standard.main.combineAsVariants([variant.mainInstance().id]),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('variant combining rejects components from another page', async (ctx) => {
|
||||||
|
const original = ctx.penpot.currentPage;
|
||||||
|
expect(original).not.toBeNull();
|
||||||
|
if (!original) return;
|
||||||
|
|
||||||
|
const originalMain = componentMain(ctx);
|
||||||
|
const otherPage = ctx.penpot.createPage();
|
||||||
|
try {
|
||||||
|
await ctx.penpot.openPage(otherPage);
|
||||||
|
const rect = ctx.penpot.createRectangle();
|
||||||
|
(otherPage.root as Board).appendChild(rect);
|
||||||
|
const other = ctx.penpot.library.local.createComponent([rect]);
|
||||||
|
const otherMain = other.mainInstance() as Board;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
ctx.penpot.createVariantFromComponents([originalMain, otherMain]),
|
||||||
|
).toThrow();
|
||||||
|
expect(() => otherMain.combineAsVariants([originalMain.id])).toThrow();
|
||||||
|
} finally {
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
otherPage.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { expect } from '../framework/expect';
|
import { expect } from '../framework/expect';
|
||||||
import { describe, test } from '../framework/registry';
|
import { describe, test } from '../framework/registry';
|
||||||
|
import type { Board } from '@penpot/plugin-types';
|
||||||
|
|
||||||
// Viewport and guides (ruler guides + board guides).
|
// Viewport and guides (ruler guides + board guides).
|
||||||
|
|
||||||
@ -80,6 +81,51 @@ describe('Ruler guides', () => {
|
|||||||
page.removeRulerGuide(guide);
|
page.removeRulerGuide(guide);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('page ruler guide rejects a removed board', (ctx) => {
|
||||||
|
const page = ctx.penpot.currentPage;
|
||||||
|
expect(page).not.toBeNull();
|
||||||
|
if (page) {
|
||||||
|
const removed = ctx.penpot.createBoard();
|
||||||
|
ctx.board.appendChild(removed);
|
||||||
|
removed.remove();
|
||||||
|
expect(() => page.addRulerGuide('vertical', 20, removed)).toThrow();
|
||||||
|
|
||||||
|
const guide = page.addRulerGuide('vertical', 20);
|
||||||
|
expect(() => {
|
||||||
|
guide.board = removed;
|
||||||
|
}).toThrow();
|
||||||
|
guide.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ruler guides reject boards from another page', async (ctx) => {
|
||||||
|
const original = ctx.penpot.currentPage;
|
||||||
|
expect(original).not.toBeNull();
|
||||||
|
if (!original) return;
|
||||||
|
|
||||||
|
const guide = original.addRulerGuide('vertical', 20);
|
||||||
|
const otherPage = ctx.penpot.createPage();
|
||||||
|
try {
|
||||||
|
await ctx.penpot.openPage(otherPage);
|
||||||
|
const otherBoard = ctx.penpot.createBoard();
|
||||||
|
(otherPage.root as Board).appendChild(otherBoard);
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
original.addRulerGuide('vertical', 20, otherBoard),
|
||||||
|
).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
guide.board = otherBoard;
|
||||||
|
}).toThrow();
|
||||||
|
} finally {
|
||||||
|
if (ctx.penpot.currentPage?.id !== original.id) {
|
||||||
|
await ctx.penpot.openPage(original);
|
||||||
|
}
|
||||||
|
guide.remove();
|
||||||
|
otherPage.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Board guides', () => {
|
describe('Board guides', () => {
|
||||||
@ -137,4 +183,66 @@ describe('Board guides', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('board guides allow automatic sizes', (ctx) => {
|
||||||
|
expect(() => {
|
||||||
|
ctx.board.guides = [
|
||||||
|
{
|
||||||
|
type: 'column',
|
||||||
|
display: true,
|
||||||
|
params: { color: { color: '#ff0000', opacity: 1 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
display: true,
|
||||||
|
params: { color: { color: '#00ff00', opacity: 1 } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'square',
|
||||||
|
display: true,
|
||||||
|
params: { color: { color: '#0000ff', opacity: 1 } },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('board guide sizes must meet their positive minimums', (ctx) => {
|
||||||
|
expect(() => {
|
||||||
|
ctx.board.guides = [
|
||||||
|
{
|
||||||
|
type: 'column',
|
||||||
|
display: true,
|
||||||
|
params: {
|
||||||
|
color: { color: '#000000', opacity: 1 },
|
||||||
|
type: 'stretch',
|
||||||
|
size: 0.5,
|
||||||
|
gutter: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
ctx.board.guides = [
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
display: true,
|
||||||
|
params: {
|
||||||
|
color: { color: '#000000', opacity: 1 },
|
||||||
|
type: 'stretch',
|
||||||
|
size: 0.5,
|
||||||
|
gutter: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}).toThrow();
|
||||||
|
expect(() => {
|
||||||
|
ctx.board.guides = [
|
||||||
|
{
|
||||||
|
type: 'square',
|
||||||
|
display: true,
|
||||||
|
params: { color: { color: '#000000', opacity: 1 }, size: 0.009 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user