40dd01c47d
Front components are still under active development, but the docs did not say so, and two of the three field reports we got on Discord were misdiagnosed because the sandbox fails silently. Adds a "Current limitations" section to the front components page covering layout measurement, DOM access, events, CSS scoping, storage and network, with the workaround for each. Also corrects the testing page, which claimed front components get "browser APIs" when the sandbox only implements a partial DOM. Every limitation was checked against the code rather than copied from the roadmap, which turned up a few stale entries: CSS imports work, `aria-*`/`data-*` now cross, and `MutationObserver` throws on `.observe()` rather than silently never firing. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23549?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
263 lines
8.7 KiB
Plaintext
263 lines
8.7 KiB
Plaintext
---
|
|
title: Testing
|
|
description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions.
|
|
icon: "flask"
|
|
---
|
|
|
|
The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server.
|
|
|
|
## Using npm packages
|
|
|
|
You can install and use any npm package in your app. Both logic functions and front components are bundled with [esbuild](https://esbuild.github.io/), which inlines all dependencies into the output — no `node_modules` are needed at runtime.
|
|
|
|
### Installing a package
|
|
|
|
```bash filename="Terminal"
|
|
yarn add axios
|
|
```
|
|
|
|
Then import it in your code:
|
|
|
|
```ts src/logic-functions/fetch-data.ts
|
|
import { defineLogicFunction } from 'twenty-sdk/define';
|
|
import axios from 'axios';
|
|
|
|
const handler = async (): Promise<any> => {
|
|
const { data } = await axios.get('https://api.example.com/data');
|
|
|
|
return { data };
|
|
};
|
|
|
|
export default defineLogicFunction({
|
|
universalIdentifier: '...',
|
|
name: 'fetch-data',
|
|
description: 'Fetches data from an external API',
|
|
timeoutSeconds: 10,
|
|
handler,
|
|
});
|
|
```
|
|
|
|
The same works for front components:
|
|
|
|
```tsx src/front-components/chart.tsx
|
|
import { defineFrontComponent } from 'twenty-sdk/define';
|
|
import { format } from 'date-fns';
|
|
|
|
const DateWidget = () => {
|
|
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
|
|
};
|
|
|
|
export default defineFrontComponent({
|
|
universalIdentifier: '...',
|
|
name: 'date-widget',
|
|
component: DateWidget,
|
|
});
|
|
```
|
|
|
|
### How bundling works
|
|
|
|
The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle.
|
|
|
|
**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed.
|
|
|
|
**Front components** run in a Web Worker. Node built-in modules are **not** available — only npm packages that work in a browser environment. Note that the sandbox implements a *partial* DOM, so a package can build cleanly and still fail at runtime; see [Current limitations](/developers/extend/apps/layout/front-components#current-limitations).
|
|
|
|
Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server.
|
|
|
|
## Setup
|
|
|
|
The scaffolded app already includes Vitest. If you set it up manually, install the dependencies:
|
|
|
|
```bash filename="Terminal"
|
|
yarn add -D vitest vite-tsconfig-paths
|
|
```
|
|
|
|
Create a `vitest.config.ts` at the root of your app:
|
|
|
|
```ts vitest.config.ts
|
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
import { defineConfig } from 'vitest/config';
|
|
|
|
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
|
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
|
|
|
|
// Make env vars available to globalSetup (test.env only applies to workers)
|
|
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
|
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
tsconfigPaths({
|
|
projects: ['tsconfig.spec.json'],
|
|
ignoreConfigErrors: true,
|
|
}),
|
|
],
|
|
test: {
|
|
testTimeout: 120_000,
|
|
hookTimeout: 120_000,
|
|
fileParallelism: false,
|
|
include: ['src/**/*.integration-test.ts'],
|
|
globalSetup: ['src/__tests__/global-setup.ts'],
|
|
env: {
|
|
TWENTY_API_URL,
|
|
TWENTY_API_KEY,
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
Create a global setup file that verifies the server is reachable, writes a test config for the SDK (`~/.twenty/config.test.json`), and syncs the app before tests run:
|
|
|
|
```ts src/__tests__/global-setup.ts
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
|
|
|
const APP_PATH = process.cwd();
|
|
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
|
|
|
export async function setup() {
|
|
const apiUrl = process.env.TWENTY_API_URL!;
|
|
const apiKey = process.env.TWENTY_API_KEY!;
|
|
|
|
// Verify the server is running
|
|
const response = await fetch(`${apiUrl}/healthz`);
|
|
if (!response.ok) {
|
|
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
|
|
}
|
|
|
|
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
|
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(CONFIG_DIR, 'config.test.json'),
|
|
JSON.stringify({
|
|
remotes: { local: { apiUrl, apiKey } },
|
|
defaultRemote: 'local',
|
|
}, null, 2),
|
|
);
|
|
|
|
// Start from a clean slate, then sync the app
|
|
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
|
|
|
const result = await appDevOnce({ appPath: APP_PATH });
|
|
if (!result.success) {
|
|
throw new Error(`Dev sync failed: ${result.error?.message}`);
|
|
}
|
|
}
|
|
|
|
export async function teardown() {
|
|
await appUninstall({ appPath: APP_PATH });
|
|
}
|
|
```
|
|
|
|
## Programmatic SDK APIs
|
|
|
|
The `twenty-sdk/cli` subpath exports functions you can call directly from test code:
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `appBuild` | Build the app and optionally pack a tarball |
|
|
| `appDeploy` | Upload a tarball to the server |
|
|
| `appDevOnce` | Build and sync the app once (same as `yarn twenty apply`) |
|
|
| `appInstall` | Install the app on the active workspace |
|
|
| `appUninstall` | Uninstall the app from the active workspace |
|
|
|
|
Each function returns a result object with `success: boolean` and either `data` or `error`.
|
|
|
|
## Writing an integration test
|
|
|
|
Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace:
|
|
|
|
```ts src/__tests__/app-install.integration-test.ts
|
|
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
|
|
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
|
|
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
const APP_PATH = process.cwd();
|
|
|
|
describe('App installation', () => {
|
|
beforeAll(async () => {
|
|
const buildResult = await appBuild({
|
|
appPath: APP_PATH,
|
|
tarball: true,
|
|
onProgress: (message: string) => console.log(`[build] ${message}`),
|
|
});
|
|
|
|
if (!buildResult.success) {
|
|
throw new Error(`Build failed: ${buildResult.error?.message}`);
|
|
}
|
|
|
|
const deployResult = await appDeploy({
|
|
tarballPath: buildResult.data.tarballPath!,
|
|
onProgress: (message: string) => console.log(`[deploy] ${message}`),
|
|
});
|
|
|
|
if (!deployResult.success) {
|
|
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
|
|
}
|
|
|
|
const installResult = await appInstall({ appPath: APP_PATH });
|
|
|
|
if (!installResult.success) {
|
|
throw new Error(`Install failed: ${installResult.error?.message}`);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await appUninstall({ appPath: APP_PATH });
|
|
});
|
|
|
|
it('should find the installed app in the workspace', async () => {
|
|
const metadataClient = new MetadataApiClient();
|
|
|
|
const result = await metadataClient.query({
|
|
findManyApplications: {
|
|
id: true,
|
|
name: true,
|
|
universalIdentifier: true,
|
|
},
|
|
});
|
|
|
|
const installedApp = result.findManyApplications.find(
|
|
(app: { universalIdentifier: string }) =>
|
|
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
);
|
|
|
|
expect(installedApp).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
## Running tests
|
|
|
|
Make sure your local Twenty server is running, then:
|
|
|
|
```bash filename="Terminal"
|
|
yarn test
|
|
```
|
|
|
|
Or in watch mode during development:
|
|
|
|
```bash filename="Terminal"
|
|
yarn test:watch
|
|
```
|
|
|
|
## Type checking
|
|
|
|
You can also run type checking on your app without running tests:
|
|
|
|
```bash filename="Terminal"
|
|
yarn twenty dev:typecheck
|
|
```
|
|
|
|
This runs `tsc --noEmit` against your app's `tsconfig.json` and reports any type errors. Scaffolded apps also ship a `yarn typecheck` script that covers test files too (`tsconfig.spec.json`).
|
|
|
|
## CI with GitHub Actions
|
|
|
|
The scaffolder generates a ready-to-use workflow at `.github/workflows/ci.yml`. On every push to `main` and every pull request, it spawns an ephemeral Twenty server in the runner (via the `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test` action), then runs `yarn lint`, `yarn typecheck`, `yarn test:unit`, and `yarn test` with `TWENTY_API_URL` / `TWENTY_API_KEY` pointing at that server. No secrets are required, and you can pin the server version via the `TWENTY_VERSION` env at the top of the workflow.
|
|
|
|
See [Publishing → Automated CI/CD](/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows) for a full walkthrough of the three scaffolded workflows (`ci.yml`, the `cd.yml` deploy pipeline, and `publish.yml` for npm publishing).
|