Alonso Torres 7655f854a4
Improved api test suite (#10533)
* 🐛 Fix plugins api issues

* 🐛 Fix problem with text weight

* 🐛 Fix api problems

*  Updated plugins test suite

* 🐛 Referential integrity tests

*  Add resetOverrides method

* 🐛 Fix plugins components structure poluting
2026-07-07 16:12:12 +02:00

160 lines
5.3 KiB
TypeScript

import { expect, expectReject } from '../framework/expect';
import { describe, test } from '../framework/registry';
import type { CommentThread, Page } from '@penpot/plugin-types';
import type { TestContext } from '../framework/types';
import { waitFor } from './wait';
// Comments.
// Comment threads are created on the current page. Thread/comment removal is
// asynchronous (it resolves once the backend delete RPC completes), so callers
// must await it; cleanup is best-effort and swallows errors.
function page(ctx: TestContext): Page {
const p = ctx.penpot.currentPage;
if (!p) throw new Error('no current page');
return p;
}
function cleanup(thread: CommentThread): void {
try {
thread.remove();
} catch (err) {
void err; // thread.remove is currently broken; ignore for cleanup
}
}
// Skipped under MOCK_BACKEND: comments assert backend-shaped responses
// (seqNumber, etc.) and pin real backend behaviour that a mock won't reproduce.
describe.skipIfMocked('Comments', () => {
test('addCommentThread creates a thread', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('Hello comment', {
x: 100,
y: 120,
});
try {
expect(typeof thread.seqNumber).toBe('number');
expect(thread.position.x).toBeCloseTo(100, 0);
expect(thread.position.y).toBeCloseTo(120, 0);
expect(thread.resolved).toBe(false);
expect(thread.owner).toBeDefined();
// A page-level thread has no board; reading it still exercises the getter.
void thread.board;
} finally {
cleanup(thread);
}
});
test('findCommentThreads lists threads', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('Find me', { x: 50, y: 50 });
try {
const threads = await p.findCommentThreads();
expect(threads.length).toBeGreaterThan(0);
} finally {
cleanup(thread);
}
});
test('reply adds a comment and findComments lists them', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('First comment', { x: 10, y: 10 });
try {
const reply = await thread.reply('A reply');
expect(reply.content).toBe('A reply');
expect(reply.user).toBeDefined();
expect(reply.date).toBeDefined();
const comments = await thread.findComments();
expect(comments.length).toBeGreaterThan(1);
} finally {
cleanup(thread);
}
});
test('thread resolved and position round-trip', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('Toggle me', { x: 30, y: 30 });
try {
thread.resolved = true;
expect(thread.resolved).toBe(true);
thread.position = { x: 200, y: 220 };
expect(thread.position.x).toBeCloseTo(200, 0);
expect(thread.position.y).toBeCloseTo(220, 0);
} finally {
cleanup(thread);
}
});
test('comment content round-trips', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('Editable', { x: 0, y: 0 });
try {
const comments = await thread.findComments();
const comment = comments[0];
comment.content = 'edited content';
// The content setter persists via an async RPC before updating locally.
await waitFor(() => comment.content === 'edited content');
expect(comment.content).toBe('edited content');
expect(comment.user).toBeDefined();
} finally {
cleanup(thread);
}
});
test('a comment can be removed', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('Keep', { x: 5, y: 5 });
try {
const reply = await thread.reply('to be removed');
await reply.remove();
// The root comment survives but the removed reply is gone.
const comments = await thread.findComments();
expect(comments.length).toBeGreaterThan(0);
expect(comments.some((c) => c.content === 'to be removed')).toBe(false);
} finally {
cleanup(thread);
}
});
test('a comment thread can be removed', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('Remove via thread', {
x: 8,
y: 8,
});
await thread.remove();
const threads = await p.findCommentThreads();
expect(threads.every((t) => t.seqNumber !== thread.seqNumber)).toBe(true);
});
test('removeCommentThread removes a thread', async (ctx) => {
const p = page(ctx);
const thread: CommentThread = await p.addCommentThread('Remove me', {
x: 70,
y: 70,
});
await p.removeCommentThread(thread);
const threads = await p.findCommentThreads();
expect(threads.every((t) => t.seqNumber !== thread.seqNumber)).toBe(true);
});
// ---------------------------------------------------------------------------
// Edge cases: empty comment content must be rejected.
// ---------------------------------------------------------------------------
test('addCommentThread with empty content rejects', async (ctx) => {
const p = page(ctx);
await expectReject(() => p.addCommentThread('', { x: 0, y: 0 }));
});
test('reply with empty content rejects', async (ctx) => {
const p = page(ctx);
const thread = await p.addCommentThread('parent', { x: 12, y: 12 });
try {
await expectReject(() => thread.reply(''));
} finally {
cleanup(thread);
}
});
});