@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 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,2 @@
1
+ import { type BasePayload, type CollectionConfig, type GlobalConfig } from 'payload';
2
+ export declare const getPayloadInstance: (collections: CollectionConfig[], globals: GlobalConfig[], tsOutputFile?: string) => Promise<BasePayload>;
@@ -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
+ };
@@ -0,0 +1,3 @@
1
+ export { getPayloadInstance } from './getPayloadInstance.js';
2
+ export { getTestContextFor } from './getTestContextFor.js';
3
+ export { setup, teardown } from './setup.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { getPayloadInstance } from './getPayloadInstance.js';
2
+ export { getTestContextFor } from './getTestContextFor.js';
3
+ export { setup, teardown } from './setup.js';
@@ -0,0 +1,3 @@
1
+ import type { TestProject } from 'vitest/node';
2
+ export declare const setup: (project: TestProject) => Promise<void>;
3
+ export declare const teardown: () => Promise<void>;
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
+ }