Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,19 @@
AP_DEV_PIECES="store,webhook"
AP_PIECES_SYNC_MODE=NONE
AP_PIECES_SOURCE='FILE'
AP_API_KEY=api-key
AP_DB_TYPE=PGLITE
AP_ENVIRONMENT=test
AP_ENCRYPTION_KEY=7e19fad4c13eaea8f657afb12e8f9c40
AP_FRONTEND_URL=http://localhost:4200
AP_WEBHOOK_TIMEOUT_SECONDS=30
AP_LOG_LEVEL=info
AP_LOG_PRETTY=true
AP_REDIS_TYPE=MEMORY
AP_TELEMETRY_ENABLED=false
AP_ENRICH_ERROR_CONTEXT=true
AP_TRIGGER_DEFAULT_POLL_INTERVAL=1
AP_CACHE_PATH=./dev/cache
AP_EXECUTION_MODE=UNSANDBOXED
AP_JWT_SECRET=secret
AP_API_KEY="api-key"

View File

@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,57 @@
import { defineConfig } from 'checkly'
import { AlertEscalationBuilder, RetryStrategyBuilder } from 'checkly/constructs'
/**
* See https://www.checklyhq.com/docs/cli/project-structure/
*/
const config = defineConfig({
/* A human friendly name for your project */
projectName: `E2E (${process.env.E2E_CONFIG_MODE})`,
/** A logical ID that needs to be unique across your Checkly account,
* See https://www.checklyhq.com/docs/cli/constructs/ to learn more about logical IDs.
*/
logicalId: `activepieces-e2e-tests-${process.env.E2E_CONFIG_MODE}`,
/* An optional URL to your Git repo to be shown in your test sessions and resource activity log */
/* repoUrl: 'https://github.com/checkly/checkly-cli', */
/* Sets default values for Checks */
checks: {
/* A default for how often your Check should run in minutes */
frequency: 10,
/* Checkly data centers to run your Checks as monitors */
locations: ['us-east-1', 'eu-west-1'],
/** The Checkly Runtime identifier, determining npm packages and the Node.js version available at runtime.
* See https://www.checklyhq.com/docs/cli/npm-packages/
*/
runtimeId: '2024.09',
/* Failed check runs will be retried before triggering alerts */
retryStrategy: RetryStrategyBuilder.fixedStrategy({ baseBackoffSeconds: 60, maxRetries: 4, sameRegion: true }),
/* A glob pattern that matches the Checks inside your repo, see https://www.checklyhq.com/docs/cli/using-check-test-match/ */
checkMatch: '**/scenarios/**/*.check.ts',
/* All checks will have this alert escalation policy defined */
alertEscalationPolicy: AlertEscalationBuilder.runBasedEscalation(1),
/* Global configuration option for Playwright-powered checks. See https://www.checklyhq.com/docs/browser-checks/playwright-test/#global-configuration */
playwrightConfig: {
use: {
baseURL: 'https://cloud.activepieces.com',
viewport: { width: 1280, height: 720 },
}
},
browserChecks: {
/* A glob pattern matches any Playwright .spec.ts files and automagically creates a Browser Check. This way, you
* can just write native Playwright code. See https://www.checklyhq.com/docs/cli/using-check-test-match/
* */
testMatch: '**/scenarios/**/*.spec.ts',
},
},
cli: {
/* The default datacenter location to use when running npx checkly test */
runLocation: 'eu-west-1',
/* An array of default reporters to use when a reporter is not specified with the "--reporter" flag */
reporters: ['list'],
/* How many times to retry a failing test run when running `npx checkly test` or `npx checkly trigger` (max. 3) */
retries: 2,
},
})
export default config

View File

@@ -0,0 +1,63 @@
import { test as base } from '@playwright/test';
import {
AuthenticationPage,
FlowsPage,
BuilderPage,
} from '../pages';
import { signUp, AuthenticationResponse } from './users';
import { DEFAULT_EMAIL, DEFAULT_PASSWORD } from '../global-setup';
type CustomFixtures = {
authenticationPage: AuthenticationPage;
flowsPage: FlowsPage;
builderPage: BuilderPage;
authenticatedPage: AuthenticationPage;
users: {
apiSignUp: () => Promise<AuthenticationResponse>;
}
};
export const test = base.extend<CustomFixtures>({
// Override page fixture to automatically authenticate before each test
page: async ({ page }, use) => {
const authPage = new AuthenticationPage(page);
if (process.env.E2E_EMAIL && process.env.E2E_PASSWORD) {
await authPage.signIn({
email: process.env.E2E_EMAIL,
password: process.env.E2E_PASSWORD,
});
} else {
await authPage.signIn({
email: DEFAULT_EMAIL,
password: DEFAULT_PASSWORD,
});
}
await use(page);
},
authenticationPage: async ({ page }, use) => {
await use(new AuthenticationPage(page));
},
flowsPage: async ({ page }, use) => {
await use(new FlowsPage(page));
},
builderPage: async ({ page }, use) => {
await use(new BuilderPage(page));
},
authenticatedPage: async ({ page }, use) => {
await use(new AuthenticationPage(page));
},
users: async ({ request, page }, use) => {
await use({
apiSignUp: async () => await signUp(request, page),
});
},
});
export { expect } from '@playwright/test';

View File

@@ -0,0 +1,50 @@
import { faker } from '@faker-js/faker';
import type { APIRequestContext, Page } from '@playwright/test';
export async function signUp(request: APIRequestContext, page: Page): Promise<AuthenticationResponse> {
const signUpPayload = {
email: faker.internet.email(),
password: '12345678',
firstName: 'Test',
lastName: 'User',
trackEvents: false,
platformId: null,
newsLetter: false,
provider: UserIdentityProvider.EMAIL,
};
const response = await request.post('/api/v1/authentication/sign-up', {
data: signUpPayload,
});
const authResponse = await response.json();
await page.addInitScript((tokenValue) => {
localStorage.setItem('token', tokenValue);
}, authResponse.token);
return authResponse;
}
// cant import from shared: https://www.checklyhq.com/docs/runtimes/#why-cant-i-import-any-npm-package-or-other-3rd-party-dependencies
export enum UserIdentityProvider {
EMAIL = 'EMAIL',
GOOGLE = 'GOOGLE',
SAML = 'SAML',
JWT = 'JWT',
}
export type AuthenticationResponse = {
id: string;
platformRole: string;
status: string;
externalId: string;
platformId: string;
verified: boolean;
firstName: string;
lastName: string;
email: string;
trackEvents: boolean;
newsLetter: boolean;
token: string;
projectId: string;
}

View File

@@ -0,0 +1,49 @@
import { chromium } from '@playwright/test';
import { AuthenticationPage } from './pages/authentication.page';
export const DEFAULT_EMAIL = 'test@activepieces.com';
export const DEFAULT_PASSWORD = 'TestPassword123!@#';
async function globalSetup() {
console.log('🔧 Running global setup...');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
baseURL: process.env.AP_FRONTEND_URL,
});
const page = await context.newPage();
const authPage = new AuthenticationPage(page);
try {
if (process.env.E2E_EMAIL && process.env.E2E_PASSWORD) {
console.log('✓ Using credentials from environment variables for sign-in');
await authPage.signIn({
email: process.env.E2E_EMAIL,
password: process.env.E2E_PASSWORD,
});
} else {
console.log('✓ Using default credentials for sign-up');
await authPage.signUp({
email: DEFAULT_EMAIL,
password: DEFAULT_PASSWORD,
firstName: 'Test',
lastName: 'User',
});
}
// Wait for successful authentication (redirect to flows page or dashboard)
await page.waitForURL('**/flows', { timeout: 15000 });
console.log('✓ Global setup completed successfully');
} catch (error) {
console.error('❌ Global setup failed:', error);
throw error;
} finally {
await context.close();
await browser.close();
}
}
export default globalSetup;

View File

@@ -0,0 +1,50 @@
import { BasePage } from './base';
import { faker } from '@faker-js/faker';
export class AuthenticationPage extends BasePage {
url = `/sign-in`;
signUpUrl = `/sign-up`;
async signIn(params: { email: string; password: string }) {
await this.page.goto(this.url);
const emailField = this.page.getByTestId('sign-in-email');
await emailField.click();
await emailField.fill(params.email);
const passwordField = this.page.getByTestId('sign-in-password');
await passwordField.click();
await passwordField.fill(params.password);
await this.page.getByTestId('sign-in-button').click();
}
async signUp(params?: { email?: string; password?: string; firstName?: string; lastName?: string }) {
await this.page.goto(this.signUpUrl);
const firstNameField = this.page.getByTestId('sign-up-first-name');
await firstNameField.click();
await firstNameField.fill(params?.firstName || 'Bugs');
await firstNameField.press('Tab');
const lastNameField = this.page.getByTestId('sign-up-last-name');
await lastNameField.click();
await lastNameField.fill(params?.lastName || 'Bunny');
await lastNameField.press('Tab');
const emailField = this.page.getByTestId('sign-up-email');
await emailField.click();
await emailField.fill(params?.email || faker.internet.email());
await emailField.press('Tab');
const passwordField = this.page.getByTestId('sign-up-password');
await passwordField.click();
await passwordField.fill(params?.password || faker.internet.password({
pattern: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?a-zA-Z0-9]/,
length: 12,
prefix: '0'
}));
await this.page.getByTestId('sign-up-button').click();
}
}

View File

@@ -0,0 +1,29 @@
import { Page, Locator } from '@playwright/test';
export interface IPageElement {
(...args: unknown[]): Locator;
}
export interface IPageAction {
(...args: unknown[]): Promise<void>;
}
export interface IPageGetter {
(...args: unknown[]): Locator;
}
export interface IPageObject {
url: string;
page: Page;
visit(): Promise<void>;
}
export abstract class BasePage implements IPageObject {
abstract url: string;
constructor(public readonly page: Page) {}
async visit(): Promise<void> {
await this.page.goto(this.url);
}
}

View File

@@ -0,0 +1,60 @@
import { expect } from '@playwright/test';
import { BasePage } from './base';
export class BuilderPage extends BasePage {
url = `/builder`;
async selectInitialTrigger(params: { piece: string; trigger: string }) {
await this.page.getByTestId('rf__node-trigger').filter({ hasText: 'Select Trigger' }).click();
await this.page.getByTestId('pieces-search-input').fill(params.trigger);
await this.page.getByText(params.trigger).click();
}
async addAction(params: { piece: string; action: string }) {
await this.page.getByTestId('add-action-button').click();
await this.page.getByTestId('pieces-search-input').fill(params.piece);
await this.page.getByTestId(params.piece).click();
await this.page.getByText(params.action).nth(1).click();
}
async testFlowAndWaitForSuccess() {
await this.page.getByRole('button', { name: 'Test Flow' }).click();
await this.page.waitForTimeout(1000);
const runSuccessLocator = this.page.locator('text=Run Succeeded');
const runSuccessText = await runSuccessLocator.textContent({ timeout: 60000 });
expect(runSuccessText).toContain('Run Succeeded');
}
async testStep() {
await this.page.getByRole('button', { name: 'Test Step Ctrl + G' }).click();
await this.page.waitForTimeout(8000);
}
async testTrigger() {
await this.page.getByTestId('test-trigger-button').click();
await this.page.waitForTimeout(5000);
}
async handleDismissButton() {
const dismissButton = this.page.getByRole('button', { name: 'Dismiss' });
if (await dismissButton.isVisible()) {
await dismissButton.click();
}
}
async loadSampleData() {
await this.page.getByText('Load Sample data').click();
await this.page.waitForTimeout(8000);
}
async publishFlow() {
await this.page.getByRole('button', { name: 'Publish' }).click();
await this.page.waitForTimeout(15000);
}
async waitFor() {
await this.page.waitForURL('**/flows/**');
await this.page.waitForSelector('.react-flow__nodes', { state: 'visible' });
await this.page.waitForSelector('.react-flow__node', { state: 'visible' });
}
}

View File

@@ -0,0 +1,30 @@
import { BasePage } from './base';
export class FlowsPage extends BasePage {
url = `/flows`;
async navigate() {
await this.page.getByRole('link', { name: 'Flows' }).click();
await this.page.waitForSelector('tbody tr');
}
async waitFor() {
await this.page.waitForSelector('tbody tr');
}
async newFlowFromScratch() {
await this.page.getByTestId('new-flow-button').click();
await this.page.getByTestId('new-flow-from-scratch-button').click();
}
async cleanupExistingFlows() {
while ((await this.page.locator('span.text-muted-foreground').count()) > 1) {
if (!(await this.page.locator('td:nth-child(7)').first().count())) break;
await this.page.locator('td:nth-child(7)').first().click();
await this.page.getByRole('menuitem', { name: 'Delete' }).click();
const confirmButton = this.page.getByRole('button', { name: 'Remove' });
await confirmButton.click();
await this.page.waitForSelector('button:has-text("Remove")', { state: 'hidden' });
await this.page.reload();
}
}
}

View File

@@ -0,0 +1,4 @@
export { BasePage } from './base';
export { AuthenticationPage } from './authentication.page';
export { FlowsPage } from './flows.page';
export { BuilderPage } from './builder.page';

View File

@@ -0,0 +1,75 @@
import { defineConfig, devices, PlaywrightTestConfig } from '@playwright/test';
import dotenv from 'dotenv';
import path from 'path';
// Load environment variables from the .env.e2e file (local dev)
// In CI, use inherited environment variables instead
if (!process.env.CI) {
dotenv.config({ path: path.resolve(__dirname, '.env.e2e') });
}
const AP_EDITION = process.env.AP_EDITION || 'ce';
const editionConfigs = {
ce: {
testDir: './scenarios/ce',
},
ee: {
testDir: './scenarios/ee',
},
};
const editionConfig = editionConfigs[AP_EDITION as keyof typeof editionConfigs];
const config: PlaywrightTestConfig = {
testDir: editionConfig.testDir,
testMatch: '**/*.spec.ts',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? '100%' : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI ? 'github' : 'html',
/* Global setup to run once before all tests */
globalSetup: require.resolve('./global-setup'),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:4200',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Run in headless mode for environments without display server */
headless: true,
},
/* Configure projects for major browsers */
projects: [
{
name: `@activepieces/${AP_EDITION}`,
use: {
...devices['Desktop Chrome'],
headless: true,
},
testDir: editionConfig.testDir,
},
],
/* Run your local dev server before starting the tests */
webServer: {
command: process.env.CI
? 'npm run dev'
: 'export $(cat .env.e2e | xargs) && npm run dev',
url: 'http://localhost:4200/api/v1/flags',
reuseExistingServer: !process.env.CI,
timeout: 100000,
stdout: 'pipe',
},
};
export default defineConfig(config);

View File

@@ -0,0 +1,37 @@
{
"name": "tests-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/tests-e2e",
"projectType": "application",
"targets": {
"test-e2e": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx playwright test --config=packages/tests-e2e/playwright.config.ts"
]
}
},
"test-checkly": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx checkly test --config=packages/tests-e2e/checkly.config.ts --reporter=github --record"
]
}
},
"deploy-checkly": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx checkly deploy"
]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
},
"tags": []
}

View File

@@ -0,0 +1,11 @@
import * as path from 'path'
import { BrowserCheck } from 'checkly/constructs'
new BrowserCheck('webhook-should-return-response-check', {
name: 'Webhook Should Return Response',
frequency: 30, // every 30 minutes
locations: ['eu-west-1'],
code: {
entrypoint: path.join(__dirname, 'webhook-should-return-response.spec.ts')
}
})

View File

@@ -0,0 +1,49 @@
import { test, expect } from '../../../fixtures';
test.describe('Webhooks', () => {
test('should handle webhook with return response', async ({ page, flowsPage, builderPage }) => {
test.setTimeout(120000);
await flowsPage.waitFor();
await flowsPage.newFlowFromScratch();
await builderPage.selectInitialTrigger({
piece: 'Webhook',
trigger: 'Catch Webhook'
});
const webhookInput = page.locator('input.grow.bg-background');
const webhookUrl = await webhookInput.inputValue();
const runVersion = Math.floor(Math.random() * 100000);
const urlWithParams = `${webhookUrl}/sync?targetRunVersion=${runVersion}`;
await builderPage.testTrigger();
await page.context().request.get(urlWithParams);
await page.waitForTimeout(5000);
await builderPage.addAction({
piece: 'Webhook',
action: 'Return Response'
});
//clear
await page.locator('div.cm-activeLine.cm-line').fill(
''
);
await page.locator('div.cm-activeLine.cm-line').fill(
'{"targetRunVersion": "{{trigger[\'queryParams\'][\'targetRunVersion\']}}"}'
);
await page.waitForTimeout(1000);
await builderPage.publishFlow();
const response = await page.context().request.get(urlWithParams);
const body = await response.json();
expect(body.targetRunVersion).toBe(runVersion.toString());
});
});

View File

@@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';
test.describe('Enterprise Edition - Placeholder Tests', () => {
test('dummy test to ensure test suite runs successfully', async () => {
expect(true).toBe(true);
});
});

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["node"]
},
"include": ["**/*.ts", "**/*.js", "playwright.config.ts"]
}