@bhofstaetter/payloadcms-integration-test-utils 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/getPayloadInstance.d.ts +2 -0
- package/dist/getPayloadInstance.js +43 -0
- package/dist/getTestContextFor.d.ts +10 -0
- package/dist/getTestContextFor.js +24 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +29 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benedikt Hofstätter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# payloadcms-integration-test-utils
|
|
2
|
+
|
|
3
|
+
Opinionated integration test helpers providing a ready-to-use Payload instance for vitest.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -D @bhofstaetter/payloadcms-integration-test-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Depending on your used database
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -D @testcontainers/mongodb
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install -D @testcontainers/postgresql
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### 1. Create a global setup file
|
|
26
|
+
|
|
27
|
+
Create a file that re-exports the `setup` and `teardown` functions:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// test/setupIntegrationTest.ts
|
|
31
|
+
export {setup, teardown} from '@bhofstaetter/payloadcms-integration-test-utils';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2. Configure Vitest
|
|
35
|
+
|
|
36
|
+
Reference the global setup file and set `DB_TYPE` plus the corresponding Docker image `MONGO_DB_IMAGE|POSTGRES_IMAGE`.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// vitest.config.ts
|
|
40
|
+
import {defineConfig} from 'vitest/config';
|
|
41
|
+
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
test: {
|
|
44
|
+
projects: [
|
|
45
|
+
{
|
|
46
|
+
test: {
|
|
47
|
+
name: 'mongodb',
|
|
48
|
+
include: ['./test/integration/**/*.test.ts'],
|
|
49
|
+
globalSetup: './test/setupIntegrationTest.ts',
|
|
50
|
+
testTimeout: 30000,
|
|
51
|
+
hookTimeout: 30000,
|
|
52
|
+
fileParallelism: false,
|
|
53
|
+
env: {
|
|
54
|
+
DB_TYPE: 'mongodb',
|
|
55
|
+
MONGO_DB_IMAGE: 'mongo:8.0',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Write tests
|
|
65
|
+
|
|
66
|
+
Use `getTestContextFor` to get a Payload Local API instance wired to the containerized database:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// test/integration/posts.test.ts
|
|
70
|
+
import type {CollectionConfig} from 'payload';
|
|
71
|
+
import {expect, it} from 'vitest';
|
|
72
|
+
import {getTestContextFor} from '@bhofstaetter/payloadcms-integration-test-utils';
|
|
73
|
+
|
|
74
|
+
const Posts: CollectionConfig = {
|
|
75
|
+
slug: 'posts',
|
|
76
|
+
fields: [{type: 'text', name: 'title'}],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ctx = getTestContextFor({collections: [Posts]});
|
|
80
|
+
|
|
81
|
+
it('creates and retrieves a document', async () => {
|
|
82
|
+
const created = await ctx.payload.create({
|
|
83
|
+
collection: 'posts',
|
|
84
|
+
data: {title: 'Hello World'},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await ctx.payload.find({collection: 'posts'});
|
|
88
|
+
|
|
89
|
+
expect(result.totalDocs).toBe(1);
|
|
90
|
+
expect(result.docs[0].id).toBe(created.id);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('starts each test with a clean database', async () => {
|
|
94
|
+
const count = await ctx.payload.count({collection: 'posts'});
|
|
95
|
+
expect(count.totalDocs).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can also test globals, or combine both:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import type {CollectionConfig, GlobalConfig} from 'payload';
|
|
103
|
+
import {getTestContextFor} from '@bhofstaetter/payloadcms-integration-test-utils';
|
|
104
|
+
|
|
105
|
+
const Posts: CollectionConfig = {
|
|
106
|
+
slug: 'posts',
|
|
107
|
+
fields: [{type: 'text', name: 'title'}],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const Settings: GlobalConfig = {
|
|
111
|
+
slug: 'settings',
|
|
112
|
+
fields: [{type: 'text', name: 'siteTitle'}],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// collections only
|
|
116
|
+
const ctx = getTestContextFor({collections: [Posts]});
|
|
117
|
+
|
|
118
|
+
// globals only
|
|
119
|
+
const ctx = getTestContextFor({globals: [Settings]});
|
|
120
|
+
|
|
121
|
+
// both
|
|
122
|
+
const ctx = getTestContextFor({collections: [Posts], globals: [Settings]});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## How It Works
|
|
126
|
+
|
|
127
|
+
### Global Setup (`setup` / `teardown`)
|
|
128
|
+
|
|
129
|
+
The `setup` function reads `DB_TYPE` from the Vitest project env and starts the corresponding Testcontainer:
|
|
130
|
+
|
|
131
|
+
| `DB_TYPE` | Container image env | Container |
|
|
132
|
+
|------------|---------------------|-----------------------|
|
|
133
|
+
| `mongodb` | `MONGO_DB_IMAGE` | `MongoDBContainer` |
|
|
134
|
+
| `postgres` | `POSTGRES_IMAGE` | `PostgreSqlContainer` |
|
|
135
|
+
|
|
136
|
+
It sets `DATABASE_URL` and `DATABASE_TYPE` as process environment variables so the Payload adapter can connect. The
|
|
137
|
+
`teardown` function stops the container.
|
|
138
|
+
|
|
139
|
+
### `getTestContextFor(options)`
|
|
140
|
+
|
|
141
|
+
Registers Vitest lifecycle hooks and returns a context object.
|
|
142
|
+
|
|
143
|
+
**Options:**
|
|
144
|
+
|
|
145
|
+
| Property | Type | Default | Description |
|
|
146
|
+
|----------------|----------------------|------------|------------------------------------|
|
|
147
|
+
| `collections` | `CollectionConfig[]` | `[]` | Collections to register and clean |
|
|
148
|
+
| `globals` | `GlobalConfig[]` | `[]` | Globals to register |
|
|
149
|
+
| `tsOutputFile` | `string` | `/dev/null`| Path to generate Payload types to |
|
|
150
|
+
|
|
151
|
+
**Lifecycle hooks:**
|
|
152
|
+
|
|
153
|
+
- **`beforeAll`** — initializes a Payload instance with the given collections and globals using the database from the global setup
|
|
154
|
+
- **`beforeEach`** — deletes all documents from every provided collection
|
|
155
|
+
- **`afterAll`** — deletes all documents from every provided collection
|
|
156
|
+
- **`ctx.payload`** — getter that returns the `BasePayload` instance for Local API calls
|
|
157
|
+
|
|
158
|
+
### `getPayloadInstance(collections, globals, tsOutputFile?)`
|
|
159
|
+
|
|
160
|
+
Lower-level function used by `getTestContextFor`. Creates (or returns a cached) Payload instance based on
|
|
161
|
+
`DATABASE_TYPE` and `DATABASE_URL` environment variables. Caches by URL so repeated calls in the same test file reuse
|
|
162
|
+
the same instance.
|
|
163
|
+
|
|
164
|
+
### Type Generation
|
|
165
|
+
|
|
166
|
+
Pass `tsOutputFile` to generate Payload types:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const ctx = getTestContextFor({collections: [Posts], tsOutputFile: './test/payload-types.ts'});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Defaults to `/dev/null` (disabled).
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
177
|
+
|
|
178
|
+
## Todo
|
|
179
|
+
|
|
180
|
+
- [ ] Github Actions
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { buildConfig, getPayload } from 'payload';
|
|
2
|
+
import { generateTypes } from 'payload/node';
|
|
3
|
+
let cachedPayload = null;
|
|
4
|
+
let cachedForUrl = null;
|
|
5
|
+
const getDbAdapter = async () => {
|
|
6
|
+
const dbType = process.env.DATABASE_TYPE;
|
|
7
|
+
if (dbType === 'mongodb') {
|
|
8
|
+
const { mongooseAdapter } = await import('@payloadcms/db-mongodb');
|
|
9
|
+
return mongooseAdapter({
|
|
10
|
+
url: process.env.DATABASE_URL || '',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
else if (dbType === 'postgres') {
|
|
14
|
+
const { postgresAdapter } = await import('@payloadcms/db-postgres');
|
|
15
|
+
return postgresAdapter({
|
|
16
|
+
pool: {
|
|
17
|
+
connectionString: process.env.DATABASE_URL || '',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
throw new Error('No or wrong DB_TYPE set. Check your vitest project setup.');
|
|
22
|
+
};
|
|
23
|
+
export const getPayloadInstance = async (collections, globals, tsOutputFile) => {
|
|
24
|
+
const currentUrl = process.env.DATABASE_URL || '';
|
|
25
|
+
if (cachedPayload && cachedForUrl === currentUrl) {
|
|
26
|
+
return cachedPayload;
|
|
27
|
+
}
|
|
28
|
+
const config = buildConfig({
|
|
29
|
+
collections,
|
|
30
|
+
globals,
|
|
31
|
+
secret: 'test-secret',
|
|
32
|
+
db: await getDbAdapter(),
|
|
33
|
+
typescript: {
|
|
34
|
+
outputFile: tsOutputFile ?? '/dev/null',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
cachedPayload = await getPayload({ config });
|
|
38
|
+
cachedForUrl = currentUrl;
|
|
39
|
+
if (tsOutputFile) {
|
|
40
|
+
await generateTypes(cachedPayload.config);
|
|
41
|
+
}
|
|
42
|
+
return cachedPayload;
|
|
43
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BasePayload, CollectionConfig, GlobalConfig } from 'payload';
|
|
2
|
+
type TestContextOptions = {
|
|
3
|
+
collections?: CollectionConfig[];
|
|
4
|
+
globals?: GlobalConfig[];
|
|
5
|
+
tsOutputFile?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const getTestContextFor: (options: TestContextOptions) => {
|
|
8
|
+
readonly payload: BasePayload;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach } from 'vitest';
|
|
2
|
+
import { getPayloadInstance } from './getPayloadInstance.js';
|
|
3
|
+
export const getTestContextFor = (options) => {
|
|
4
|
+
const { collections = [], globals = [], tsOutputFile } = options;
|
|
5
|
+
let payload;
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
payload = await getPayloadInstance(collections, globals, tsOutputFile);
|
|
8
|
+
});
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
for (const collection of collections) {
|
|
11
|
+
await payload.delete({ collection: collection.slug, where: {} });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
for (const collection of collections) {
|
|
16
|
+
await payload.delete({ collection: collection.slug, where: {} });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
get payload() {
|
|
21
|
+
return payload;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/setup.d.ts
ADDED
package/dist/setup.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
let container;
|
|
2
|
+
export const setup = async (project) => {
|
|
3
|
+
const dbType = project.config.env.DB_TYPE;
|
|
4
|
+
process.env.DATABASE_TYPE = dbType;
|
|
5
|
+
if (dbType === 'mongodb') {
|
|
6
|
+
const mongoImage = project.config.env.MONGO_DB_IMAGE;
|
|
7
|
+
if (!mongoImage) {
|
|
8
|
+
throw new Error('MONGO_DB_IMAGE is not set. Check your vitest project setup.');
|
|
9
|
+
}
|
|
10
|
+
const { MongoDBContainer } = await import('@testcontainers/mongodb');
|
|
11
|
+
container = await new MongoDBContainer(mongoImage).start();
|
|
12
|
+
process.env.DATABASE_URL = `${container.getConnectionString?.()}?directConnection=true`;
|
|
13
|
+
}
|
|
14
|
+
else if (dbType === 'postgres') {
|
|
15
|
+
const postgresImage = project.config.env.POSTGRES_IMAGE;
|
|
16
|
+
if (!postgresImage) {
|
|
17
|
+
throw new Error('POSTGRES_IMAGE is not set. Check your vitest project setup.');
|
|
18
|
+
}
|
|
19
|
+
const { PostgreSqlContainer } = await import('@testcontainers/postgresql');
|
|
20
|
+
container = await new PostgreSqlContainer(postgresImage).start();
|
|
21
|
+
process.env.DATABASE_URL = container.getConnectionUri?.();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw new Error('No or wrong DB_TYPE set. Check your vitest project setup.');
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const teardown = async () => {
|
|
28
|
+
await container?.stop();
|
|
29
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhofstaetter/payloadcms-integration-test-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Opinionated integration test helpers providing a ready-to-use Payload instance for vitest.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Benedikt Hofstätter",
|
|
7
|
+
"homepage": "https://github.com/bhofstaetter/payloadcms-integration-test-utils",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/bhofstaetter/payloadcms-integration-test-utils.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"payload",
|
|
14
|
+
"payloadcms",
|
|
15
|
+
"local api",
|
|
16
|
+
"testing",
|
|
17
|
+
"integration test",
|
|
18
|
+
"mongodb",
|
|
19
|
+
"postgres",
|
|
20
|
+
"vitest"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
34
|
+
"typecheck": "tsc",
|
|
35
|
+
"lint": "biome check",
|
|
36
|
+
"lint:fix": "biome check --fix",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"payload": ">=3",
|
|
42
|
+
"vitest": ">=4"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "2.4.4",
|
|
46
|
+
"@payloadcms/db-postgres": "3.78.0",
|
|
47
|
+
"@payloadcms/db-mongodb": "3.78.0",
|
|
48
|
+
"@testcontainers/postgresql": "^11.12.0",
|
|
49
|
+
"@testcontainers/mongodb": "^11.12.0",
|
|
50
|
+
"payload": "3.78.0",
|
|
51
|
+
"typescript": "^5",
|
|
52
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
53
|
+
"vitest": "^4.0.18"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|