mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-01-02 04:40:11 -05:00
feat: init e2e test
This commit is contained in:
parent
0eac44dd93
commit
b0af7051ad
18 changed files with 6123 additions and 7016 deletions
|
@ -1,2 +1,2 @@
|
|||
ACCESS_TOKEN = ''
|
||||
API_URL = 'http://localhost:3449/api/rpc/command'
|
||||
E2E_LOGIN_EMAIL=""
|
||||
E2E_LOGIN_PASSWORD=""
|
||||
|
|
32
apps/e2e/eslint.config.js
Normal file
32
apps/e2e/eslint.config.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import baseConfig from '../../eslint.config.js';
|
||||
import typescriptEslintParser from '@typescript-eslint/parser';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptEslintParser,
|
||||
parserOptions: { project: './apps/e2e/tsconfig.json' },
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js', '**/*.jsx'],
|
||||
rules: {},
|
||||
},
|
||||
{ ignores: ['vite.config.ts'] },
|
||||
];
|
8
apps/e2e/project.json
Normal file
8
apps/e2e/project.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "e2e",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"implicitDependencies": [],
|
||||
"tags": ["type:e2e"],
|
||||
"targets": {}
|
||||
}
|
512
apps/e2e/src/__snapshots__/plugins.spec.ts.snap
Normal file
512
apps/e2e/src/__snapshots__/plugins.spec.ts.snap
Normal file
|
@ -0,0 +1,512 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Plugins > create frame - text - rectable 1`] = `
|
||||
[
|
||||
{
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#FFFFFF",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":flip-x": null,
|
||||
":flip-y": null,
|
||||
":frame-id": "1",
|
||||
":height": 0.01,
|
||||
":hide-fill-on-export": false,
|
||||
":id": "1",
|
||||
":name": "Root Frame",
|
||||
":parent-id": "1",
|
||||
":points": [
|
||||
{
|
||||
":x": 0,
|
||||
":y": 0,
|
||||
},
|
||||
{
|
||||
":x": 0.01,
|
||||
":y": 0,
|
||||
},
|
||||
{
|
||||
":x": 0.01,
|
||||
":y": 0.01,
|
||||
},
|
||||
{
|
||||
":x": 0,
|
||||
":y": 0.01,
|
||||
},
|
||||
],
|
||||
":proportion": 1,
|
||||
":proportion-lock": false,
|
||||
":rotation": 0,
|
||||
":selrect": {
|
||||
":height": 0.01,
|
||||
":width": 0.01,
|
||||
":x": 0,
|
||||
":x1": 0,
|
||||
":x2": 0.01,
|
||||
":y": 0,
|
||||
":y1": 0,
|
||||
":y2": 0.01,
|
||||
},
|
||||
":shapes": [
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
],
|
||||
":strokes": [],
|
||||
":transform": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":transform-inverse": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":type": ":frame",
|
||||
":width": 0.01,
|
||||
":x": 0,
|
||||
":y": 0,
|
||||
},
|
||||
{
|
||||
":content": {
|
||||
":children": [
|
||||
{
|
||||
":children": [
|
||||
{
|
||||
":children": [
|
||||
{
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#000000",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":font-family": "sourcesanspro",
|
||||
":font-id": "sourcesanspro",
|
||||
":font-size": "14",
|
||||
":font-style": "normal",
|
||||
":font-variant-id": "regular",
|
||||
":font-weight": "400",
|
||||
":letter-spacing": "0",
|
||||
":line-height": "1.2",
|
||||
":text": "Hello from plugin",
|
||||
":text-align": "left",
|
||||
":text-decoration": "none",
|
||||
":text-transform": "none",
|
||||
":typography-ref-file": null,
|
||||
":typography-ref-id": null,
|
||||
},
|
||||
],
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#000000",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":font-family": "sourcesanspro",
|
||||
":font-id": "sourcesanspro",
|
||||
":font-size": "14",
|
||||
":font-style": "normal",
|
||||
":font-variant-id": "regular",
|
||||
":font-weight": "400",
|
||||
":letter-spacing": "0",
|
||||
":line-height": "1.2",
|
||||
":text-align": "left",
|
||||
":text-decoration": "none",
|
||||
":text-transform": "none",
|
||||
":type": "paragraph",
|
||||
":typography-ref-file": null,
|
||||
":typography-ref-id": null,
|
||||
},
|
||||
],
|
||||
":type": "paragraph-set",
|
||||
},
|
||||
],
|
||||
":type": "root",
|
||||
},
|
||||
":flip-x": null,
|
||||
":flip-y": null,
|
||||
":frame-id": "1",
|
||||
":grow-type": ":auto-width",
|
||||
":height": 17,
|
||||
":id": "2",
|
||||
":name": "Text",
|
||||
":parent-id": "1",
|
||||
":points": [
|
||||
{
|
||||
":x": 684,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":x": 786,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":x": 786,
|
||||
":y": 557,
|
||||
},
|
||||
{
|
||||
":x": 684,
|
||||
":y": 557,
|
||||
},
|
||||
],
|
||||
":position-data": [
|
||||
{
|
||||
":direction": "ltr",
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#000000",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":font-family": "sourcesanspro",
|
||||
":font-size": "14px",
|
||||
":font-style": "normal",
|
||||
":font-weight": "400",
|
||||
":height": 18,
|
||||
":letter-spacing": "normal",
|
||||
":text": "Hello from plugin",
|
||||
":text-decoration": "none solid rgb(0, 0, 0)",
|
||||
":text-transform": "none",
|
||||
":width": 101.5313,
|
||||
":x": 684,
|
||||
":x1": 0,
|
||||
":x2": 101.5313,
|
||||
":y": 557,
|
||||
":y1": -1,
|
||||
":y2": 17,
|
||||
},
|
||||
],
|
||||
":rotation": 0,
|
||||
":selrect": {
|
||||
":height": 17,
|
||||
":width": 102,
|
||||
":x": 684,
|
||||
":x1": 684,
|
||||
":x2": 786,
|
||||
":y": 540,
|
||||
":y1": 540,
|
||||
":y2": 557,
|
||||
},
|
||||
":transform": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":transform-inverse": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":type": ":text",
|
||||
":width": 102,
|
||||
":x": 684,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#B1B2B5",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":flip-x": null,
|
||||
":flip-y": null,
|
||||
":frame-id": "1",
|
||||
":height": 200,
|
||||
":id": "3",
|
||||
":name": "Rectangle",
|
||||
":parent-id": "1",
|
||||
":plugin-data": {
|
||||
":plugin/TEST": {
|
||||
"customKey": "customValue",
|
||||
},
|
||||
},
|
||||
":points": [
|
||||
{
|
||||
":x": 684,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":x": 884,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":x": 884,
|
||||
":y": 740,
|
||||
},
|
||||
{
|
||||
":x": 684,
|
||||
":y": 740,
|
||||
},
|
||||
],
|
||||
":proportion": 1,
|
||||
":proportion-lock": false,
|
||||
":rotation": 0,
|
||||
":rx": 0,
|
||||
":ry": 0,
|
||||
":selrect": {
|
||||
":height": 200,
|
||||
":width": 200,
|
||||
":x": 684,
|
||||
":x1": 684,
|
||||
":x2": 884,
|
||||
":y": 540,
|
||||
":y1": 540,
|
||||
":y2": 740,
|
||||
},
|
||||
":strokes": [],
|
||||
":transform": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":transform-inverse": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":type": ":rect",
|
||||
":width": 200,
|
||||
":x": 684,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#FFFFFF",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":flip-x": null,
|
||||
":flip-y": null,
|
||||
":frame-id": "1",
|
||||
":height": 300,
|
||||
":hide-fill-on-export": false,
|
||||
":id": "4",
|
||||
":name": "Frame name",
|
||||
":parent-id": "1",
|
||||
":points": [
|
||||
{
|
||||
":x": 684,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":x": 984,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":x": 984,
|
||||
":y": 840,
|
||||
},
|
||||
{
|
||||
":x": 684,
|
||||
":y": 840,
|
||||
},
|
||||
],
|
||||
":proportion": 1,
|
||||
":proportion-lock": false,
|
||||
":rotation": 0,
|
||||
":rx": 8,
|
||||
":ry": 8,
|
||||
":selrect": {
|
||||
":height": 300,
|
||||
":width": 300,
|
||||
":x": 684,
|
||||
":x1": 684,
|
||||
":x2": 984,
|
||||
":y": 540,
|
||||
":y1": 540,
|
||||
":y2": 840,
|
||||
},
|
||||
":shapes": [
|
||||
"5",
|
||||
],
|
||||
":strokes": [],
|
||||
":transform": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":transform-inverse": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":type": ":frame",
|
||||
":width": 300,
|
||||
":x": 684,
|
||||
":y": 540,
|
||||
},
|
||||
{
|
||||
":constraints-h": ":left",
|
||||
":constraints-v": ":top",
|
||||
":content": {
|
||||
":children": [
|
||||
{
|
||||
":children": [
|
||||
{
|
||||
":children": [
|
||||
{
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#000000",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":font-family": "sourcesanspro",
|
||||
":font-id": "sourcesanspro",
|
||||
":font-size": "14",
|
||||
":font-style": "normal",
|
||||
":font-variant-id": "regular",
|
||||
":font-weight": "400",
|
||||
":letter-spacing": "0",
|
||||
":line-height": "1.2",
|
||||
":text": "Hello from frame",
|
||||
":text-align": "left",
|
||||
":text-decoration": "none",
|
||||
":text-transform": "none",
|
||||
":typography-ref-file": null,
|
||||
":typography-ref-id": null,
|
||||
},
|
||||
],
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#000000",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":font-family": "sourcesanspro",
|
||||
":font-id": "sourcesanspro",
|
||||
":font-size": "14",
|
||||
":font-style": "normal",
|
||||
":font-variant-id": "regular",
|
||||
":font-weight": "400",
|
||||
":letter-spacing": "0",
|
||||
":line-height": "1.2",
|
||||
":text-align": "left",
|
||||
":text-decoration": "none",
|
||||
":text-transform": "none",
|
||||
":type": "paragraph",
|
||||
":typography-ref-file": null,
|
||||
":typography-ref-id": null,
|
||||
},
|
||||
],
|
||||
":type": "paragraph-set",
|
||||
},
|
||||
],
|
||||
":type": "root",
|
||||
},
|
||||
":flip-x": null,
|
||||
":flip-y": null,
|
||||
":frame-id": "4",
|
||||
":grow-type": ":auto-width",
|
||||
":height": 17,
|
||||
":id": "5",
|
||||
":name": "Text",
|
||||
":parent-id": "4",
|
||||
":points": [
|
||||
{
|
||||
":x": 10,
|
||||
":y": 10,
|
||||
},
|
||||
{
|
||||
":x": 109,
|
||||
":y": 10,
|
||||
},
|
||||
{
|
||||
":x": 109,
|
||||
":y": 27,
|
||||
},
|
||||
{
|
||||
":x": 10,
|
||||
":y": 27,
|
||||
},
|
||||
],
|
||||
":position-data": [
|
||||
{
|
||||
":direction": "ltr",
|
||||
":fills": [
|
||||
{
|
||||
":fill-color": "#000000",
|
||||
":fill-opacity": 1,
|
||||
},
|
||||
],
|
||||
":font-family": "sourcesanspro",
|
||||
":font-size": "14px",
|
||||
":font-style": "normal",
|
||||
":font-weight": "400",
|
||||
":height": 18,
|
||||
":letter-spacing": "normal",
|
||||
":text": "Hello from frame",
|
||||
":text-decoration": "none solid rgb(0, 0, 0)",
|
||||
":text-transform": "none",
|
||||
":width": 98.7344,
|
||||
":x": 10,
|
||||
":x1": 0,
|
||||
":x2": 98.7344,
|
||||
":y": 27,
|
||||
":y1": -1,
|
||||
":y2": 17,
|
||||
},
|
||||
],
|
||||
":rotation": 0,
|
||||
":selrect": {
|
||||
":height": 17,
|
||||
":width": 99,
|
||||
":x": 10,
|
||||
":x1": 10,
|
||||
":x2": 109,
|
||||
":y": 10,
|
||||
":y1": 10,
|
||||
":y2": 27,
|
||||
},
|
||||
":transform": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":transform-inverse": {
|
||||
":a": 1,
|
||||
":b": 0,
|
||||
":c": 0,
|
||||
":d": 1,
|
||||
":e": 0,
|
||||
":f": 0,
|
||||
},
|
||||
":type": ":text",
|
||||
":width": 99,
|
||||
":x": 10,
|
||||
":y": 10,
|
||||
},
|
||||
]
|
||||
`;
|
17
apps/e2e/src/models/file-rpc.model.ts
Normal file
17
apps/e2e/src/models/file-rpc.model.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export interface FileRpc {
|
||||
'~:name': string;
|
||||
'~:revn': number;
|
||||
'~:id': string;
|
||||
'~:is-shared': boolean;
|
||||
'~:version': number;
|
||||
'~:project-id': string;
|
||||
'~:data': {
|
||||
'~:pages': string[];
|
||||
'~:objects': string[];
|
||||
'~:styles': string[];
|
||||
'~:components': string[];
|
||||
'~:styles-v2': string[];
|
||||
'~:components-v2': string[];
|
||||
'~:features': string[];
|
||||
};
|
||||
}
|
11
apps/e2e/src/plugins.spec.ts
Normal file
11
apps/e2e/src/plugins.spec.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import testingPlugin from './plugins/create-frame-text-rect';
|
||||
import { Agent } from './utils/agent';
|
||||
|
||||
describe('Plugins', () => {
|
||||
it('create frame - text - rectable', async () => {
|
||||
const agent = await Agent();
|
||||
const result = await agent.runCode(testingPlugin.toString());
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
64
apps/e2e/src/plugins/create-frame-text-rect.ts
Normal file
64
apps/e2e/src/plugins/create-frame-text-rect.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import type {
|
||||
PenpotFrame,
|
||||
PenpotRectangle,
|
||||
PenpotText,
|
||||
} from '@penpot/plugin-types';
|
||||
|
||||
export default function () {
|
||||
function createText(text: string): PenpotText | undefined {
|
||||
const textNode = penpot.createText(text);
|
||||
|
||||
if (!textNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
textNode.x = penpot.viewport.center.x;
|
||||
textNode.y = penpot.viewport.center.y;
|
||||
|
||||
return textNode;
|
||||
}
|
||||
|
||||
function createRectangle(): PenpotRectangle {
|
||||
const rectangle = penpot.createRectangle();
|
||||
|
||||
rectangle.setPluginData('customKey', 'customValue');
|
||||
|
||||
rectangle.x = penpot.viewport.center.x;
|
||||
rectangle.y = penpot.viewport.center.y;
|
||||
|
||||
rectangle.resize(200, 200);
|
||||
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
function createFrame(): PenpotFrame {
|
||||
const frame = penpot.createFrame();
|
||||
|
||||
frame.name = 'Frame name';
|
||||
|
||||
console.log(penpot.viewport.center.x);
|
||||
|
||||
frame.x = penpot.viewport.center.x;
|
||||
frame.y = penpot.viewport.center.y;
|
||||
|
||||
frame.borderRadius = 8;
|
||||
|
||||
frame.resize(300, 300);
|
||||
|
||||
const text = penpot.createText('Hello from frame');
|
||||
|
||||
if (!text) {
|
||||
throw new Error('Could not create text');
|
||||
}
|
||||
|
||||
text.x = 10;
|
||||
text.y = 10;
|
||||
frame.appendChild(text);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
createText('Hello from plugin');
|
||||
createRectangle();
|
||||
createFrame();
|
||||
}
|
155
apps/e2e/src/utils/agent.ts
Normal file
155
apps/e2e/src/utils/agent.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import puppeteer from 'puppeteer';
|
||||
import { PenpotApi } from './api';
|
||||
import { getFileUrl } from './get-file-url';
|
||||
|
||||
interface Shape {
|
||||
':id': string;
|
||||
':frame-id'?: string;
|
||||
':parent-id'?: string;
|
||||
':shapes'?: string[];
|
||||
}
|
||||
|
||||
function replaceIds(shapes: Shape[]) {
|
||||
let id = 1;
|
||||
|
||||
const getId = () => {
|
||||
return String(id++);
|
||||
};
|
||||
|
||||
function replaceChildrenId(id: string, newId: string) {
|
||||
for (const node of shapes) {
|
||||
if (node[':parent-id'] === id) {
|
||||
node[':parent-id'] = newId;
|
||||
}
|
||||
|
||||
if (node[':frame-id'] === id) {
|
||||
node[':frame-id'] = newId;
|
||||
}
|
||||
|
||||
if (node[':shapes']) {
|
||||
node[':shapes'] = node[':shapes']?.map((shapeId) => {
|
||||
return shapeId === id ? newId : shapeId;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of shapes) {
|
||||
const previousId = node[':id'] as string;
|
||||
|
||||
node[':id'] = getId();
|
||||
|
||||
replaceChildrenId(previousId, node[':id']);
|
||||
}
|
||||
}
|
||||
|
||||
export async function Agent() {
|
||||
console.log('Initializing Penpot API...');
|
||||
const penpotApi = await PenpotApi();
|
||||
|
||||
console.log('Creating file...');
|
||||
const file = await penpotApi.createFile();
|
||||
console.log('File created with id:', file['~:id']);
|
||||
|
||||
const fileUrl = getFileUrl(file);
|
||||
console.log('File URL:', fileUrl);
|
||||
|
||||
console.log('Launching browser...');
|
||||
const browser = await puppeteer.launch({});
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
console.log('Setting authentication cookie...');
|
||||
page.setCookie({
|
||||
name: 'auth-token',
|
||||
value: penpotApi.getAuth().split('=')[1],
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
expires: (Date.now() + 3600 * 1000) / 1000,
|
||||
});
|
||||
|
||||
console.log('Navigating to file URL...');
|
||||
await page.goto(fileUrl);
|
||||
await page.waitForSelector('[data-testid="viewport"]');
|
||||
console.log('Page loaded and viewport selector found.');
|
||||
|
||||
page
|
||||
.on('console', async (message) => {
|
||||
console.log(`${message.type()} ${message.text()}`);
|
||||
})
|
||||
.on('pageerror', (message) => {
|
||||
console.error('Page error:', message);
|
||||
});
|
||||
|
||||
const finish = async () => {
|
||||
console.log('Deleting file and closing browser...');
|
||||
await penpotApi.deleteFile(file['~:id']);
|
||||
await browser.close();
|
||||
console.log('Clean up done.');
|
||||
};
|
||||
|
||||
return {
|
||||
async runCode(
|
||||
code: string,
|
||||
options: { screenshot?: string; autoFinish?: boolean } = {
|
||||
screenshot: '',
|
||||
autoFinish: true,
|
||||
}
|
||||
) {
|
||||
console.log('Running plugin code...');
|
||||
await page.evaluate((testingPlugin) => {
|
||||
(globalThis as any).ɵloadPlugin({
|
||||
pluginId: 'TEST',
|
||||
name: 'Test',
|
||||
code: `
|
||||
(${testingPlugin})();
|
||||
`,
|
||||
icon: '',
|
||||
description: '',
|
||||
permissions: ['content:read', 'content:write'],
|
||||
});
|
||||
}, code);
|
||||
|
||||
console.log('Waiting for save status...');
|
||||
await page.waitForSelector(
|
||||
'.main_ui_workspace_right_header__saved-status',
|
||||
{
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
console.log('Save status found.');
|
||||
|
||||
if (options.screenshot) {
|
||||
console.log('Taking screenshot:', options.screenshot);
|
||||
await page.screenshot({ path: options.screenshot });
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
page.once('console', async (msg) => {
|
||||
const args = (await Promise.all(
|
||||
msg.args().map((arg) => arg.jsonValue())
|
||||
)) as Record<string, unknown>[];
|
||||
|
||||
const result = Object.values(args[1]) as Shape[];
|
||||
|
||||
replaceIds(result);
|
||||
console.log('IDs replaced in result.');
|
||||
|
||||
resolve(result);
|
||||
|
||||
if (options.autoFinish) {
|
||||
console.log('Auto finish enabled. Cleaning up...');
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Evaluating debug.dump_objects...');
|
||||
page.evaluate(`
|
||||
debug.dump_objects();
|
||||
`);
|
||||
});
|
||||
},
|
||||
finish,
|
||||
};
|
||||
}
|
85
apps/e2e/src/utils/api.ts
Normal file
85
apps/e2e/src/utils/api.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { FileRpc } from '../models/file-rpc.model';
|
||||
|
||||
const apiUrl = 'http://localhost:3449';
|
||||
|
||||
export async function PenpotApi() {
|
||||
if (!process.env['E2E_LOGIN_EMAIL']) {
|
||||
throw new Error('E2E_LOGIN_EMAIL not set');
|
||||
}
|
||||
|
||||
const resultLoginRequest = await fetch(
|
||||
`${apiUrl}/api/rpc/command/login-with-password`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/transit+json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'~:email': process.env['E2E_LOGIN_EMAIL'],
|
||||
'~:password': process.env['E2E_LOGIN_PASSWORD'],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const loginData = await resultLoginRequest.json();
|
||||
const authToken = resultLoginRequest.headers
|
||||
.get('set-cookie')
|
||||
?.split(';')
|
||||
.at(0);
|
||||
|
||||
if (!authToken) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
return {
|
||||
getAuth: () => authToken,
|
||||
createFile: async () => {
|
||||
const createFileRequest = await fetch(
|
||||
`${apiUrl}/api/rpc/command/create-file`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/transit+json',
|
||||
cookie: authToken,
|
||||
credentials: 'include',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'~:name': `test file ${new Date().toISOString()}`,
|
||||
'~:project-id': loginData['~:default-project-id'],
|
||||
'~:features': {
|
||||
'~#set': [
|
||||
'fdata/objects-map',
|
||||
'fdata/pointer-map',
|
||||
'fdata/shape-data-type',
|
||||
'components/v2',
|
||||
'styles/v2',
|
||||
'layout/grid',
|
||||
'plugins/runtime',
|
||||
],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return (await createFileRequest.json()) as FileRpc;
|
||||
},
|
||||
deleteFile: async (fileId: string) => {
|
||||
const deleteFileRequest = await fetch(
|
||||
`${apiUrl}/api/rpc/command/delete-file`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/transit+json',
|
||||
cookie: authToken,
|
||||
credentials: 'include',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'~:id': fileId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return deleteFileRequest;
|
||||
},
|
||||
};
|
||||
}
|
3
apps/e2e/src/utils/clean-id.ts
Normal file
3
apps/e2e/src/utils/clean-id.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function cleanId(id: string) {
|
||||
return id.replace('~u', '');
|
||||
}
|
10
apps/e2e/src/utils/get-file-url.ts
Normal file
10
apps/e2e/src/utils/get-file-url.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { FileRpc } from '../models/file-rpc.model';
|
||||
import { cleanId } from './clean-id';
|
||||
|
||||
export function getFileUrl(file: FileRpc) {
|
||||
const projectId = cleanId(file['~:project-id']);
|
||||
const fileId = cleanId(file['~:id']);
|
||||
const pageId = cleanId(file['~:data']['~:pages'][0]);
|
||||
|
||||
return `http://localhost:3449/#/workspace/${projectId}/${fileId}?page-id=${pageId}`;
|
||||
}
|
27
apps/e2e/tsconfig.json
Normal file
27
apps/e2e/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vitest.config.ts",
|
||||
"src/**/*.ts",
|
||||
"../../libs/plugin-types/index.d.ts"
|
||||
]
|
||||
}
|
23
apps/e2e/vite.config.ts
Normal file
23
apps/e2e/vite.config.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/e2e',
|
||||
test: {
|
||||
testTimeout: 20000,
|
||||
watch: false,
|
||||
globals: true,
|
||||
cache: {
|
||||
dir: '../node_modules/.vitest',
|
||||
},
|
||||
environment: 'happy-dom',
|
||||
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../coverage/e2e',
|
||||
provider: 'v8',
|
||||
},
|
||||
setupFiles: ['dotenv/config'],
|
||||
},
|
||||
});
|
81
docs/test-e2e.md
Normal file
81
docs/test-e2e.md
Normal file
|
@ -0,0 +1,81 @@
|
|||
## End-to-End (E2E) Testing Guide
|
||||
|
||||
### Setting Up
|
||||
|
||||
1. **Configure Environment Variables**
|
||||
|
||||
Create and populate the `.env` file with a valid user mail & password:
|
||||
|
||||
```env
|
||||
E2E_LOGIN_EMAIL="test@penpot.app"
|
||||
E2E_LOGIN_PASSWORD="123123123"
|
||||
```
|
||||
|
||||
2. **Run E2E Tests**
|
||||
|
||||
Use the following command to execute the E2E tests:
|
||||
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
1. **Adding Tests**
|
||||
|
||||
Place your test files in the `/apps/e2e/src/**/*.spec.ts` directory. Below is an example of a test file:
|
||||
|
||||
```ts
|
||||
import testingPlugin from './plugins/create-frame-text-rect';
|
||||
import { Agent } from './utils/agent';
|
||||
|
||||
describe('Plugins', () => {
|
||||
it('create frame - text - rectangle', async () => {
|
||||
const agent = await Agent();
|
||||
const result = await agent.runCode(testingPlugin.toString());
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
|
||||
- `Agent` opens a browser, logs into Penpot, and creates a file.
|
||||
- `runCode` executes the plugin code and returns the file state after execution.
|
||||
|
||||
2. **Using `runCode` Method**
|
||||
|
||||
The `runCode` method takes the plugin code as a string:
|
||||
|
||||
```ts
|
||||
const result = await agent.runCode(testingPlugin.toString());
|
||||
```
|
||||
|
||||
It can also accept an options object:
|
||||
|
||||
```ts
|
||||
const result = await agent.runCode(testingPlugin.toString(), {
|
||||
autoFinish: false, // default: true
|
||||
screenshot: 'capture.png', // default: ''
|
||||
});
|
||||
|
||||
// Finish will close the browser & delete the file
|
||||
agent.finish();
|
||||
```
|
||||
|
||||
3. **Snapshot Testing**
|
||||
|
||||
The `toMatchSnapshot` method stores the result and throws an error if the content does not match the previous result:
|
||||
|
||||
```ts
|
||||
expect(result).toMatchSnapshot();
|
||||
```
|
||||
|
||||
Snapshots are stored in the `apps/e2e/src/__snapshots__/*.spec.ts.snap` directory.
|
||||
|
||||
If you need to refresh all the snapshopts run the test with the update option:
|
||||
|
||||
```bash
|
||||
npm run test:e2e -- --update
|
||||
```
|
|
@ -44,6 +44,10 @@ export default [
|
|||
sourceTag: 'type:util',
|
||||
onlyDependOnLibsWithTags: ['type:util'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'type:e2e',
|
||||
onlyDependOnLibsWithTags: ['type:ui', 'type:util'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -24,6 +24,10 @@ export function loadManifest(url: string): Promise<Manifest> {
|
|||
}
|
||||
|
||||
export function loadManifestCode(manifest: Manifest): Promise<string> {
|
||||
if (!manifest.host && !manifest.code.startsWith('http')) {
|
||||
return Promise.resolve(manifest.code);
|
||||
}
|
||||
|
||||
return fetch(getValidUrl(manifest.host, manifest.code)).then((response) => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
|
|
12097
package-lock.json
generated
12097
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -21,6 +21,7 @@
|
|||
"lint": "nx run-many --all --target=lint --parallel",
|
||||
"lint:affected": "npx nx affected --target=lint",
|
||||
"test": "nx run-many -t test --parallel -p plugins-runtime lorem-ipsum-plugin",
|
||||
"test:e2e": "npx nx test e2e",
|
||||
"registry": "nx local-registry",
|
||||
"prepare": "husky",
|
||||
"create:api-docs": "npx typedoc --tsconfig libs/plugins-runtime/tsconfig.lib.json --customCss ./tools/typedoc.css",
|
||||
|
@ -95,6 +96,7 @@
|
|||
"@angular/router": "18.0.1",
|
||||
"axios": "^1.6.0",
|
||||
"feather-icons": "^4.29.2",
|
||||
"puppeteer": "^22.11.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"ses": "^1.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
|
Loading…
Reference in a new issue