🐛 Add more test for reported bugs (#10765)

This commit is contained in:
Alonso Torres 2026-07-21 12:18:40 +02:00 committed by GitHub
parent 2de08f00e8
commit 6c5d01283a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 2 deletions

View File

@ -1043,7 +1043,12 @@
ids (into #{} (map #(obj/get % "$id")) shapes)]
(st/emit! (-> (dwl/add-component id-ref ids)
(se/add-event plugin-id)))
(lib-component-proxy plugin-id file-id @id-ref))))
;; add-component only sets id-ref when it actually creates a
;; component; an empty selection or shapes that can't form one
;; leave it nil, so reject instead of returning a broken proxy.
(if-let [id @id-ref]
(lib-component-proxy plugin-id file-id id)
(u/not-valid plugin-id :createComponent "Cannot create a component from the given shapes")))))
;; Plugin data
:getPluginData

View File

@ -6,6 +6,7 @@
### 🩹 Fixes
- **plugins-runtime**: `Library.createComponent()` now rejects invalid input (an empty shape list, or a shape inside a component copy) with a validation error instead of returning a component proxy pointing at nothing.
- **plugins-runtime**: Setting an individual padding/margin side (`leftPadding`, `topMargin`, …) now re-derives the padding/margin type, switching to `multiple` when the four sides stop being symmetric (so the value is actually painted) and back to `simple` once top/bottom and left/right are mirrored again.
## 1.5.0 (2026-07-08)

View File

@ -1,6 +1,6 @@
import { expect } from '../framework/expect';
import { describe, test } from '../framework/registry';
import type { Text } from '@penpot/plugin-types';
import type { Board, Text } from '@penpot/plugin-types';
import type { TestContext } from '../framework/types';
import { PNG_1X1 } from './fixtures';
@ -310,5 +310,59 @@ describe('Library', () => {
expect(instance).toBeDefined();
expect(typeof instance.id).toBe('string');
});
// Reported (via MCP): create a page-root shape (rectangle / ellipse / text),
// save it as a component, then use it from Assets — the component root was
// said to be a plain shape instead of a board/component. This does not
// reproduce: a lone page-root shape is wrapped in a board and that board
// becomes the component (main-instance) root. Pin the invariant for the
// exact repro.
test('createComponent wraps a page-root shape in a board component root', (ctx) => {
// A genuine page-root shape: created but NOT placed inside any board.
const rect = ctx.penpot.createRectangle();
const comp = ctx.penpot.library.local.createComponent([rect]);
const main = comp.mainInstance() as Board;
expect(main.type).toBe('board');
expect(main.isComponentRoot()).toBeTruthy();
expect(main.isComponentMainInstance()).toBeTruthy();
expect(main.children.some((c) => c.id === rect.id)).toBe(true);
const errors = ctx.penpot.currentFile?.validate() ?? [];
expect(errors.map((e) => e.code)).toEqual([]);
// The generated main instance lands at the page root (outside ctx.board);
// tuck it under the scratch board so teardown removes it.
ctx.board.appendChild(main);
});
// createComponent must reject inputs that cannot form a component rather
// than succeeding silently or crashing. Both cases below currently surface
// an INTERNAL assertion ("Assert failed: (uuid? id)"): the operation is a
// no-op, so the returned LibraryComponent proxy is built from a nil id. It
// should be a clean rejection (or a null return) instead — and in a release
// build, where the assertion is elided, the caller gets a broken component
// proxy pointing at nothing. The file must stay valid regardless.
test('createComponent with no shapes is rejected', (ctx) => {
expect(() => ctx.penpot.library.local.createComponent([])).toThrow();
const errors = ctx.penpot.currentFile?.validate() ?? [];
expect(errors.map((e) => e.code)).toEqual([]);
});
test('createComponent from a shape inside a component copy is rejected', (ctx) => {
const rect = ctx.penpot.createRectangle();
ctx.board.appendChild(rect);
const source = ctx.penpot.library.local.createComponent([rect]);
const copy = source.instance() as Board;
ctx.board.appendChild(copy);
const copyChild = copy.children[0];
expect(copyChild).toBeDefined();
expect(() =>
ctx.penpot.library.local.createComponent([copyChild]),
).toThrow();
const errors = ctx.penpot.currentFile?.validate() ?? [];
expect(errors.map((e) => e.code)).toEqual([]);
});
});
});