mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-01-19 21:22:32 -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 = ''
|
E2E_LOGIN_EMAIL=""
|
||||||
API_URL = 'http://localhost:3449/api/rpc/command'
|
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',
|
sourceTag: 'type:util',
|
||||||
onlyDependOnLibsWithTags: ['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> {
|
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) => {
|
return fetch(getValidUrl(manifest.host, manifest.code)).then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.text();
|
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": "nx run-many --all --target=lint --parallel",
|
||||||
"lint:affected": "npx nx affected --target=lint",
|
"lint:affected": "npx nx affected --target=lint",
|
||||||
"test": "nx run-many -t test --parallel -p plugins-runtime lorem-ipsum-plugin",
|
"test": "nx run-many -t test --parallel -p plugins-runtime lorem-ipsum-plugin",
|
||||||
|
"test:e2e": "npx nx test e2e",
|
||||||
"registry": "nx local-registry",
|
"registry": "nx local-registry",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"create:api-docs": "npx typedoc --tsconfig libs/plugins-runtime/tsconfig.lib.json --customCss ./tools/typedoc.css",
|
"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",
|
"@angular/router": "18.0.1",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
|
"puppeteer": "^22.11.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"ses": "^1.5.0",
|
"ses": "^1.5.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
|
Loading…
Add table
Reference in a new issue