mirror of
https://github.com/penpot/penpot.git
synced 2026-10-30 22:40:33 +00:00
* ✨ Add missing plugin data validations * ✨ Add migration to fix the new schema validations * ✨ Add end-to-end tests for plugins validation * 🐛 Fix unit tests after merge * 🐛 Change normalize behavior
664 lines
21 KiB
TypeScript
664 lines
21 KiB
TypeScript
import { expect } from '../framework/expect';
|
|
import { describe, test } from '../framework/registry';
|
|
import type { Board, Rectangle } from '@penpot/plugin-types';
|
|
import type { TestContext } from '../framework/types';
|
|
|
|
// Interactions, overlays and animations.
|
|
// Interactions are added to a shape; navigate/overlay actions target boards, so
|
|
// destination boards are self-provisioned on the scratch board.
|
|
|
|
function board(ctx: TestContext): Board {
|
|
const b = ctx.penpot.createBoard();
|
|
ctx.board.appendChild(b);
|
|
return b;
|
|
}
|
|
|
|
function rect(ctx: TestContext): Rectangle {
|
|
const r = ctx.penpot.createRectangle();
|
|
ctx.board.appendChild(r);
|
|
return r;
|
|
}
|
|
|
|
describe('Interactions', () => {
|
|
test('navigate-to interaction round-trips', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
});
|
|
|
|
expect(interaction.trigger).toBe('click');
|
|
expect(interaction.action.type).toBe('navigate-to');
|
|
if (interaction.action.type === 'navigate-to') {
|
|
expect(interaction.action.destination.id).toBe(dest.id);
|
|
}
|
|
expect(interaction.shape && interaction.shape.id).toBe(r.id);
|
|
expect(r.interactions.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('open-url interaction round-trips', (ctx) => {
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'open-url',
|
|
url: 'https://example.com',
|
|
});
|
|
expect(interaction.action.type).toBe('open-url');
|
|
if (interaction.action.type === 'open-url') {
|
|
expect(interaction.action.url).toBe('https://example.com');
|
|
}
|
|
});
|
|
|
|
test('open-overlay interaction round-trips', (ctx) => {
|
|
const overlay = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'open-overlay',
|
|
destination: overlay,
|
|
position: 'manual',
|
|
manualPositionLocation: { x: 10, y: 20 },
|
|
closeWhenClickOutside: true,
|
|
addBackgroundOverlay: true,
|
|
animation: { type: 'dissolve', duration: 100, easing: 'linear' },
|
|
});
|
|
expect(interaction.action.type).toBe('open-overlay');
|
|
if (interaction.action.type === 'open-overlay') {
|
|
expect(interaction.action.destination.id).toBe(overlay.id);
|
|
expect(interaction.action.position).toBe('manual');
|
|
expect(interaction.action.closeWhenClickOutside).toBe(true);
|
|
expect(interaction.action.addBackgroundOverlay).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('open-overlay supports a non-manual position', (ctx) => {
|
|
const overlay = board(ctx);
|
|
const r = rect(ctx);
|
|
// Per the types, manualPositionLocation is only needed for 'manual'.
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'open-overlay',
|
|
destination: overlay,
|
|
position: 'center',
|
|
animation: { type: 'dissolve', duration: 100, easing: 'linear' },
|
|
});
|
|
expect(interaction.action.type).toBe('open-overlay');
|
|
});
|
|
|
|
// position is optional; when omitted the overlay defaults to 'center'.
|
|
test('open-overlay without a position defaults to center', (ctx) => {
|
|
const overlay = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'open-overlay',
|
|
destination: overlay,
|
|
});
|
|
expect(interaction.action.type).toBe('open-overlay');
|
|
if (interaction.action.type === 'open-overlay') {
|
|
expect(interaction.action.destination.id).toBe(overlay.id);
|
|
expect(interaction.action.position).toBe('center');
|
|
}
|
|
});
|
|
|
|
test('toggle-overlay interaction round-trips', (ctx) => {
|
|
const overlay = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'toggle-overlay',
|
|
destination: overlay,
|
|
position: 'manual',
|
|
manualPositionLocation: { x: 0, y: 0 },
|
|
animation: { type: 'dissolve', duration: 100, easing: 'linear' },
|
|
});
|
|
expect(interaction.action.type).toBe('toggle-overlay');
|
|
if (interaction.action.type === 'toggle-overlay') {
|
|
expect(interaction.action.destination.id).toBe(overlay.id);
|
|
}
|
|
});
|
|
|
|
test('close-overlay interaction round-trips', (ctx) => {
|
|
const overlay = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'close-overlay',
|
|
destination: overlay,
|
|
animation: { type: 'dissolve', duration: 200, easing: 'linear' },
|
|
});
|
|
expect(interaction.action.type).toBe('close-overlay');
|
|
if (interaction.action.type === 'close-overlay') {
|
|
expect(
|
|
interaction.action.destination && interaction.action.destination.id,
|
|
).toBe(overlay.id);
|
|
expect(interaction.action.animation).toBeDefined();
|
|
}
|
|
});
|
|
|
|
// animation is optional on close-overlay; omitting it closes with no transition.
|
|
test('close-overlay without an animation round-trips', (ctx) => {
|
|
const overlay = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'close-overlay',
|
|
destination: overlay,
|
|
});
|
|
expect(interaction.action.type).toBe('close-overlay');
|
|
if (interaction.action.type === 'close-overlay') {
|
|
expect(
|
|
interaction.action.destination && interaction.action.destination.id,
|
|
).toBe(overlay.id);
|
|
expect(interaction.action.animation).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
test('previous-screen interaction round-trips', (ctx) => {
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', { type: 'previous-screen' });
|
|
expect(interaction.action.type).toBe('previous-screen');
|
|
});
|
|
|
|
test('after-delay trigger carries a delay', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = board(ctx);
|
|
const interaction = r.addInteraction(
|
|
'after-delay',
|
|
{ type: 'navigate-to', destination: dest },
|
|
1000,
|
|
);
|
|
expect(interaction.trigger).toBe('after-delay');
|
|
expect(interaction.delay).toBeCloseTo(1000, 0);
|
|
});
|
|
|
|
// A zero delay is a valid value (fires immediately), not an error.
|
|
test('after-delay accepts a zero delay', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = board(ctx);
|
|
const interaction = r.addInteraction(
|
|
'after-delay',
|
|
{ type: 'navigate-to', destination: dest },
|
|
0,
|
|
);
|
|
expect(interaction.trigger).toBe('after-delay');
|
|
expect(interaction.delay).toBeCloseTo(0, 0);
|
|
});
|
|
|
|
test('mouse-leave trigger is recorded', (ctx) => {
|
|
// click / mouse-enter / after-delay are covered above; mouse-leave is the
|
|
// remaining trigger.
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('mouse-leave', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
});
|
|
expect(interaction.trigger).toBe('mouse-leave');
|
|
});
|
|
|
|
// Pins persistence of the `delay` and `action` setters on an existing
|
|
// interaction (mutating after `addInteraction`). `misc.test.ts:300` exercises
|
|
// these setters' (set) coverage targets but never asserts that the new values
|
|
// stick; this fills that behavioural gap. (An older note claimed these setters
|
|
// "don't persist" — that is stale: CI confirms they do.)
|
|
test('interaction delay and action setters persist', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = board(ctx);
|
|
const interaction = r.addInteraction(
|
|
'after-delay',
|
|
{ type: 'navigate-to', destination: dest },
|
|
1000,
|
|
);
|
|
|
|
interaction.delay = 250;
|
|
interaction.action = { type: 'previous-screen' };
|
|
|
|
// Re-fetch from the shape so we prove persistence, not that the proxy
|
|
// reflects its own write.
|
|
const persisted = r.interactions[0];
|
|
expect(persisted.delay).toBeCloseTo(250, 0);
|
|
expect(persisted.action.type).toBe('previous-screen');
|
|
});
|
|
|
|
// The delay setter accepts zero (fires immediately) as a valid value.
|
|
test('delay setter accepts a zero value', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = board(ctx);
|
|
const interaction = r.addInteraction(
|
|
'after-delay',
|
|
{ type: 'navigate-to', destination: dest },
|
|
1000,
|
|
);
|
|
|
|
interaction.delay = 0;
|
|
expect(interaction.delay).toBeCloseTo(0, 0);
|
|
});
|
|
|
|
test('removeInteraction removes an interaction from a shape', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
});
|
|
const before = r.interactions.length;
|
|
r.removeInteraction(interaction);
|
|
expect(r.interactions.length).toBe(before - 1);
|
|
});
|
|
|
|
describe('Animations', () => {
|
|
test('dissolve animation round-trips', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
animation: { type: 'dissolve', duration: 300, easing: 'ease' },
|
|
});
|
|
if (
|
|
interaction.action.type === 'navigate-to' &&
|
|
interaction.action.animation
|
|
) {
|
|
expect(interaction.action.animation.type).toBe('dissolve');
|
|
if (interaction.action.animation.type === 'dissolve') {
|
|
expect(interaction.action.animation.duration).toBeCloseTo(300, 0);
|
|
expect(interaction.action.animation.easing).toBe('ease');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('dissolve animation accepts every easing curve', (ctx) => {
|
|
// Only `linear` and `ease` are exercised elsewhere; cover the remaining
|
|
// easing curves so a single broken curve is caught.
|
|
for (const easing of ['ease-in', 'ease-out', 'ease-in-out'] as const) {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
animation: { type: 'dissolve', duration: 200, easing },
|
|
});
|
|
if (
|
|
interaction.action.type === 'navigate-to' &&
|
|
interaction.action.animation &&
|
|
interaction.action.animation.type === 'dissolve'
|
|
) {
|
|
expect(interaction.action.animation.easing).toBe(easing);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('slide animation round-trips', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
animation: {
|
|
type: 'slide',
|
|
way: 'in',
|
|
direction: 'right',
|
|
duration: 300,
|
|
easing: 'linear',
|
|
},
|
|
});
|
|
if (
|
|
interaction.action.type === 'navigate-to' &&
|
|
interaction.action.animation
|
|
) {
|
|
expect(interaction.action.animation.type).toBe('slide');
|
|
if (interaction.action.animation.type === 'slide') {
|
|
expect(interaction.action.animation.way).toBe('in');
|
|
expect(interaction.action.animation.direction).toBe('right');
|
|
expect(interaction.action.animation.duration).toBeCloseTo(300, 0);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('push animation round-trips', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
animation: {
|
|
type: 'push',
|
|
direction: 'left',
|
|
duration: 300,
|
|
easing: 'linear',
|
|
},
|
|
});
|
|
if (
|
|
interaction.action.type === 'navigate-to' &&
|
|
interaction.action.animation
|
|
) {
|
|
expect(interaction.action.animation.type).toBe('push');
|
|
if (interaction.action.animation.type === 'push') {
|
|
expect(interaction.action.animation.direction).toBe('left');
|
|
expect(interaction.action.animation.duration).toBeCloseTo(300, 0);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test('an interaction can be removed', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
});
|
|
|
|
const before = r.interactions.length;
|
|
interaction.remove();
|
|
expect(r.interactions.length).toBe(before - 1);
|
|
});
|
|
|
|
// Removing an interaction shifts the ones after it, so draining a shape from
|
|
// a single read of the array must reach every interaction it returned. Both
|
|
// removal entry points are covered.
|
|
test('every interaction can be removed from one read of the array', async (ctx) => {
|
|
const r = rect(ctx);
|
|
r.addInteraction('click', { type: 'open-url', url: 'https://a.example' });
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
r.addInteraction('mouse-enter', {
|
|
type: 'open-url',
|
|
url: 'https://b.example',
|
|
});
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
expect(r.interactions).toHaveLength(2);
|
|
|
|
for (const interaction of r.interactions) {
|
|
interaction.remove();
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
}
|
|
expect(r.interactions).toHaveLength(0);
|
|
});
|
|
|
|
test('removeInteraction can drain a shape from one read of the array', async (ctx) => {
|
|
const r = rect(ctx);
|
|
r.addInteraction('click', { type: 'open-url', url: 'https://a.example' });
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
r.addInteraction('mouse-enter', {
|
|
type: 'open-url',
|
|
url: 'https://b.example',
|
|
});
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
expect(r.interactions).toHaveLength(2);
|
|
|
|
for (const interaction of r.interactions) {
|
|
r.removeInteraction(interaction);
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
}
|
|
expect(r.interactions).toHaveLength(0);
|
|
});
|
|
|
|
// A held interaction addresses itself rather than a position, so a write
|
|
// reaches it even once an earlier interaction has shifted it.
|
|
test('an interaction still writes to itself after an earlier one is removed', async (ctx) => {
|
|
const r = rect(ctx);
|
|
for (const trigger of ['click', 'mouse-enter', 'mouse-leave'] as const) {
|
|
r.addInteraction(trigger, {
|
|
type: 'open-url',
|
|
url: `https://${trigger}.example`,
|
|
});
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
}
|
|
const [first, , last] = r.interactions;
|
|
|
|
first.remove();
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
last.delay = 500;
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
|
|
expect(r.interactions.map((i) => i.trigger)).toEqual([
|
|
'mouse-enter',
|
|
'mouse-leave',
|
|
]);
|
|
expect(r.interactions.map((i) => i.delay)).toEqual([null, 500]);
|
|
});
|
|
|
|
// A proxy whose own interaction is gone resolves to no position, so the write
|
|
// has nothing to address and the API rejects it.
|
|
test('a write through a removed interaction is rejected', async (ctx) => {
|
|
const r = rect(ctx);
|
|
r.addInteraction('click', {
|
|
type: 'open-url',
|
|
url: 'https://example.com',
|
|
});
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
const [only] = r.interactions;
|
|
|
|
only.remove();
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
expect(r.interactions).toHaveLength(0);
|
|
|
|
expect(() => {
|
|
only.delay = 500;
|
|
}).toThrow('The interaction is not part of the shape anymore');
|
|
await ctx.penpot.waitForLayoutUpdate();
|
|
expect(r.interactions).toHaveLength(0);
|
|
});
|
|
|
|
test('interaction trigger can be changed', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
const interaction = r.addInteraction('click', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
});
|
|
|
|
interaction.trigger = '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
|
|
// rejected; the "success" test checks several triggers coexisting.
|
|
// ---------------------------------------------------------------------------
|
|
test('navigate-to a removed board throws', (ctx) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
dest.remove();
|
|
expect(() =>
|
|
r.addInteraction('click', { type: 'navigate-to', destination: dest }),
|
|
).toThrow();
|
|
});
|
|
|
|
test('open-url rejects invalid input and normalizes a bare hostname', (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', {
|
|
type: 'open-url',
|
|
url: 'example.com/path',
|
|
});
|
|
expect(interaction.action.type).toBe('open-url');
|
|
if (interaction.action.type === 'open-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) => {
|
|
const dest = board(ctx);
|
|
const r = rect(ctx);
|
|
r.addInteraction('click', { type: 'navigate-to', destination: dest });
|
|
r.addInteraction('mouse-enter', {
|
|
type: 'navigate-to',
|
|
destination: dest,
|
|
});
|
|
expect(r.interactions).toHaveLength(2);
|
|
expect(r.interactions.map((i) => i.trigger).sort()).toEqual([
|
|
'click',
|
|
'mouse-enter',
|
|
]);
|
|
});
|
|
});
|