# Testing apps

Automated tests catch formula bugs, action logic errors, and field definition mistakes before they reach production. This guide covers how to set up and run Jest tests for Comind.work app logic.

## Running tests[​](#running-tests "Direct link to Running tests")

Run all tests from the project root:

```
npm test
```

Run tests for a single app:

```
npm test -- --testPathPatterns=app-org-task
```

Tests use Jest with TypeScript support via the project's shared configuration. No extra setup is needed beyond the standard project dependencies.

## What you can test[​](#what-you-can-test "Direct link to What you can test")

| Component                               | Testable? | How                                                                        |
| --------------------------------------- | --------- | -------------------------------------------------------------------------- |
| Calc field functions                    | Yes       | Import the function, pass a mock entity, assert the result                 |
| Action logic functions                  | Yes       | Import the function, set up `entity`/`entityOld` globals, assert mutations |
| Precondition functions                  | Yes       | Import the function, set up `entity` global, assert the boolean return     |
| Field definitions                       | Yes       | Import the fields array, assert structure (names, types, options)          |
| View logic (`getInvisibleFields`, etc.) | Limited   | Functions depend on `ComindView` global which is not available in tests    |
| Layouts (TSX)                           | No        | Layouts compile to XML strings at build time, not testable in isolation    |

Focus your testing effort on calc fields and action logic - these contain the business rules that matter most.

## Test file location[​](#test-file-location "Direct link to Test file location")

Place test files next to the code they test, using the `.test.ts` extension:

```
app-order/
├── fields/
│   └── calc-fields/
│       ├── calc-total.ts
│       └── calc-total.test.ts    <-- test file
└── actions/
    └── logic/
        ├── index.ts
        └── index.test.ts         <-- test file
```

This co-location makes it easy to find tests and keeps imports short.

## Example: testing a calc field[​](#example-testing-a-calc-field "Direct link to Example: testing a calc field")

Calc field functions live in `fields/calc-fields/` and are exported as named functions. They read from the `entity` global and return a computed value. To test one, declare `entity` as a global, assign it a mock object, and call the function.

Here is a calc field that computes a line total from quantity and unit price:

```
// fields/calc-fields/calc-total.ts
export function c_total() {
    if (entity.c_quantity == null || entity.c_unit_price == null) {
        return 0;
    }
    return entity.c_quantity * entity.c_unit_price;
}
```

The field definition in `fields/index.ts` references this function by name:

```
{
    calcType: 'calcfield',
    caption: 'Total',
    dependsOn: ['c_quantity', 'c_unit_price'],
    name: 'c_total',
    options: { formatting_pre: '$', decimal_places: 2 },
    type: 'number',
},
```

And the test:

```
// fields/calc-fields/calc-total.test.ts
import { c_total } from './calc-total';

declare let entity: any;

beforeEach(() => {
    (globalThis as any).entity = {};
});

afterEach(() => {
    delete (globalThis as any).entity;
});

describe('c_total', () => {
    it('calculates total as quantity times unit price', () => {
        entity.c_quantity = 5;
        entity.c_unit_price = 12.5;
        expect(c_total()).toBe(62.5);
    });

    it('returns 0 when quantity is null', () => {
        entity.c_quantity = null;
        entity.c_unit_price = 10;
        expect(c_total()).toBe(0);
    });

    it('returns 0 when unit price is null', () => {
        entity.c_quantity = 3;
        entity.c_unit_price = null;
        expect(c_total()).toBe(0);
    });

    it('returns 0 when both fields are missing', () => {
        expect(c_total()).toBe(0);
    });

    it('handles zero quantity', () => {
        entity.c_quantity = 0;
        entity.c_unit_price = 25;
        expect(c_total()).toBe(0);
    });

    it('handles fractional quantities', () => {
        entity.c_quantity = 2.5;
        entity.c_unit_price = 10;
        expect(c_total()).toBe(25);
    });
});
```

The `beforeEach` / `afterEach` pattern ensures each test starts with a clean `entity` global. This mirrors how the platform injects `entity` at runtime.

## Example: testing action logic[​](#example-testing-action-logic "Direct link to Example: testing action logic")

Action logic functions run server-side and mutate the `entity` global directly. They also have access to `entityOld` (the record's previous state) and `currentUser`. To test them, set up these globals before calling the function.

Here is an action logic module with an `approve` function:

```
// actions/logic/index.ts
import { entity } from '#typings';
import type { ActionLogic } from '@comind/api';

export default {
    approve,
    reject,
} satisfies ActionLogic<typeof entity>;

export function approve() {
    entity.c_approved_by = currentUser.id;
    entity.c_approved_date = new Date().toISOString();
}

export function reject() {
    entity.c_rejection_reason =
        entity.c_rejection_reason || 'No reason provided';
}
```

And the test:

```
// actions/logic/index.test.ts
import { approve, reject } from './index';

declare let entity: any;
declare let entityOld: any;
declare let currentUser: any;

beforeEach(() => {
    (globalThis as any).entity = {};
    (globalThis as any).entityOld = {};
    (globalThis as any).currentUser = { id: 'user-42' };
});

afterEach(() => {
    delete (globalThis as any).entity;
    delete (globalThis as any).entityOld;
    delete (globalThis as any).currentUser;
});

describe('approve', () => {
    it('sets approved_by to the current user', () => {
        approve();
        expect(entity.c_approved_by).toBe('user-42');
    });

    it('sets approved_date to an ISO string', () => {
        approve();
        expect(entity.c_approved_date).toMatch(
            /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
        );
    });
});

describe('reject', () => {
    it('sets default rejection reason when none provided', () => {
        reject();
        expect(entity.c_rejection_reason).toBe('No reason provided');
    });

    it('preserves existing rejection reason', () => {
        entity.c_rejection_reason = 'Incomplete documentation';
        reject();
        expect(entity.c_rejection_reason).toBe('Incomplete documentation');
    });
});
```

Note that `entity` and `entityOld` are globals - you set them up before calling the function, then assert the mutations afterward. This is the same execution model the platform uses at runtime.

## Example: testing preconditions[​](#example-testing-preconditions "Direct link to Example: testing preconditions")

Precondition functions return a boolean that determines whether an action is available. Test them by setting `entity` (and optionally `currentUser`) to different states and checking the return value.

Here is a precondition module:

```
// actions/logic/preconditions.ts
import { entity } from '#typings';
import type { PreconditionLogic } from '@comind/api';

export default {
    approve: canApprove,
    submit: canSubmit,
} satisfies PreconditionLogic<typeof entity>;

export function canApprove() {
    return entity.state === 'pending' && currentUser.isInGroup('Reviewers');
}

export function canSubmit() {
    return entity.state === 'draft';
}
```

And the test:

```
// actions/logic/preconditions.test.ts
import { canApprove, canSubmit } from './preconditions';

declare let entity: any;
declare let currentUser: any;

beforeEach(() => {
    (globalThis as any).entity = {};
    (globalThis as any).currentUser = {
        isInGroup: (group: string) => group === 'Reviewers',
    };
});

afterEach(() => {
    delete (globalThis as any).entity;
    delete (globalThis as any).currentUser;
});

describe('canApprove', () => {
    it('returns true when state is pending and user is a reviewer', () => {
        entity.state = 'pending';
        expect(canApprove()).toBe(true);
    });

    it('returns false when state is not pending', () => {
        entity.state = 'draft';
        expect(canApprove()).toBe(false);
    });

    it('returns false when user is not in Reviewers group', () => {
        entity.state = 'pending';
        (globalThis as any).currentUser = {
            isInGroup: () => false,
        };
        expect(canApprove()).toBe(false);
    });
});

describe('canSubmit', () => {
    it('returns true when state is draft', () => {
        entity.state = 'draft';
        expect(canSubmit()).toBe(true);
    });

    it('returns false when state is pending', () => {
        entity.state = 'pending';
        expect(canSubmit()).toBe(false);
    });
});
```

## Example: testing field definitions[​](#example-testing-field-definitions "Direct link to Example: testing field definitions")

Field definition tests verify that your `AppField[]` array follows naming conventions and includes required fields. These tests act as a safety net against typos and missing configuration.

```
// fields/index.test.ts
import { fields } from './index';

describe('field definitions', () => {
    it('all custom fields use the c_ prefix', () => {
        const systemFields = [
            'title', 'description', 'state', 'creation_date',
            'update_date', 'keeper_id',
        ];
        for (const field of fields) {
            if (!systemFields.includes(field.name)) {
                expect(field.name).toMatch(/^c_/);
            }
        }
    });

    it('includes required fields', () => {
        const fieldNames = fields.map((f) => f.name);
        expect(fieldNames).toContain('c_quantity');
        expect(fieldNames).toContain('c_unit_price');
        expect(fieldNames).toContain('c_status');
    });

    it('lookup fields have properly formatted options', () => {
        const lookups = fields.filter((f) => f.type === 'lookup');
        for (const lookup of lookups) {
            expect(lookup.options?.lookupOptions).toBeDefined();
            for (const opt of lookup.options!.lookupOptions!) {
                expect(opt).toHaveProperty('name');
                expect(opt).toHaveProperty('caption');
                expect(opt.name).toMatch(/^[a-z0-9_-]+$/);
            }
        }
    });

    it('calc fields declare their dependencies', () => {
        const calcFields = fields.filter((f) => f.calcType === 'calcfield');
        for (const field of calcFields) {
            expect(field.dependsOn).toBeDefined();
            expect(field.dependsOn!.length).toBeGreaterThan(0);
        }
    });
});
```

## Tips[​](#tips "Direct link to Tips")

* **Keep tests fast.** They should run in seconds, not minutes. Avoid network calls or file system access.
* **Test business logic, not framework behavior.** Do not test that the platform calls your calc field - test that the calc field returns the right value.
* **Calc field tests are the most valuable.** They catch formula bugs before deployment. A wrong multiplication or a missing null check is easy to miss in code review but trivial to catch with a test.
* **Action logic tests catch bugs in state changes.** If an approval action should set three fields, a test ensures all three are set correctly.
* **Use descriptive test names.** Write `"calculates total as quantity times unit price"` instead of `"works correctly"`. When a test fails, the name should tell you what broke.
* **Mock globals in `beforeEach`, clean up in `afterEach`.** This prevents state leaking between tests and mirrors how the platform provides `entity`, `entityOld`, and `currentUser` at runtime.
* **Export functions you want to test.** The default export (`satisfies ActionLogic`) is consumed by the platform, but you can also export individual functions by name for direct testing. See the [action logic](/developer-guide/building-blocks/actions.md) and [precondition](/developer-guide/building-blocks/actions.md#preconditions) patterns.

## Related[​](#related "Direct link to Related")

* [Production deployment](/developer-guide/how-to/advanced/production-deployment.md) - pre-deploy checklist and multi-environment configuration
* [Fields and field options](/developer-guide/building-blocks/fields-and-field-options.md) - field definitions and calc field types referenced in field definition tests
