mirror of
https://github.com/penpot/penpot.git
synced 2026-08-10 23:08:41 +00:00
* 🐛 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
160 lines
5.3 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|