@dungarees/test-environment 0.11.4
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/constants.d.ts +1 -0
- package/constants.js +1 -0
- package/cucumber-test-environment.d.ts +11 -0
- package/cucumber-test-environment.js +63 -0
- package/guards.d.ts +6 -0
- package/guards.js +5 -0
- package/interactors/browser.d.ts +19 -0
- package/interactors/browser.js +54 -0
- package/interactors/network.d.ts +5 -0
- package/interactors/network.js +34 -0
- package/interactors/node-command-line.d.ts +23 -0
- package/interactors/node-command-line.js +99 -0
- package/package.json +85 -0
- package/runners/dev-server.d.ts +6 -0
- package/runners/dev-server.js +19 -0
- package/runners/node-process.d.ts +10 -0
- package/runners/node-process.js +44 -0
- package/runners/npm-registry.d.ts +10 -0
- package/runners/npm-registry.js +70 -0
- package/runners/postgres.d.ts +8 -0
- package/runners/postgres.js +37 -0
- package/test-environment.d.ts +17 -0
- package/test-environment.js +100 -0
- package/test-environment.test.d.ts +1 -0
- package/test-environment.test.js +442 -0
- package/type.d.ts +50 -0
- package/type.js +1 -0
- package/world.d.ts +8 -0
- package/world.js +35 -0
package/constants.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const NODE_CONTAINER_IMAGE: "node:24-bullseye";
|
package/constants.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NODE_CONTAINER_IMAGE = 'node:24-bullseye';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServiceConfig } from './type.ts';
|
|
2
|
+
import type { TestEnvironmentWorld } from './world.ts';
|
|
3
|
+
type CucumberTestEnvironment<SERVICES extends Record<string, ServiceConfig>> = {
|
|
4
|
+
Given: (step: string, callback: (world: TestEnvironmentWorld<SERVICES>) => Promise<void>) => void;
|
|
5
|
+
When: (step: string, callback: (world: TestEnvironmentWorld<SERVICES>) => Promise<void>) => void;
|
|
6
|
+
Then: (step: string, callback: (world: TestEnvironmentWorld<SERVICES>) => Promise<void>) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare const createCucumberTestEnvironment: <SERVICES extends Record<string, ServiceConfig>>(services: SERVICES, options?: {
|
|
9
|
+
timeout?: number;
|
|
10
|
+
}) => CucumberTestEnvironment<SERVICES>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createTestEnvironment } from './test-environment.js';
|
|
2
|
+
import { After, AfterAll, Before, BeforeAll, Given as CucumberGiven, Then as CucumberThen, When as CucumberWhen, setDefaultTimeout, setWorldConstructor, Status, World, } from '@cucumber/cucumber';
|
|
3
|
+
export const createCucumberTestEnvironment = (services, options = {}) => {
|
|
4
|
+
setDefaultTimeout(options.timeout ?? 60 * 1000);
|
|
5
|
+
const testEnvironment = createTestEnvironment(services);
|
|
6
|
+
class TestEnvironmentWorld extends World {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.debug = false;
|
|
10
|
+
this.testEnvironmentWorld = testEnvironment.createWorld();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
setWorldConstructor(TestEnvironmentWorld);
|
|
14
|
+
BeforeAll(async function () {
|
|
15
|
+
const entry$ = await testEnvironment.onBeforeAll();
|
|
16
|
+
entry$.subscribe(({ entry, type }) => {
|
|
17
|
+
console.log(entry, type);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
Before({ tags: '@ignore' }, function () {
|
|
21
|
+
return 'skipped';
|
|
22
|
+
});
|
|
23
|
+
Before({ tags: '@debug' }, function () {
|
|
24
|
+
this.debug = true;
|
|
25
|
+
});
|
|
26
|
+
Before(async function () {
|
|
27
|
+
const entry$ = await testEnvironment.onBefore(this.testEnvironmentWorld);
|
|
28
|
+
entry$.subscribe(({ entry, type }) => {
|
|
29
|
+
this.attach(entry, type);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
After(async function ({ result, pickle }) {
|
|
33
|
+
if (result?.status !== Status.PASSED) {
|
|
34
|
+
const testName = pickle.name.replace(/\W/g, '-');
|
|
35
|
+
const entries = await testEnvironment.onFailure(this.testEnvironmentWorld, testName);
|
|
36
|
+
for (const { entry, type } of entries) {
|
|
37
|
+
this.attach(entry, type);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.attach(`Status: ${result?.status ?? ''}. Duration:${result?.duration?.seconds ?? ''}s`);
|
|
41
|
+
const entry$ = await testEnvironment.onAfter(this.testEnvironmentWorld);
|
|
42
|
+
entry$.subscribe(({ entry, type }) => {
|
|
43
|
+
this.attach(entry, type);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
AfterAll(async function () {
|
|
47
|
+
const entry$ = await testEnvironment.onAfterAll();
|
|
48
|
+
entry$.subscribe(({ entry, type }) => {
|
|
49
|
+
console.log(entry, type);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
Given: (step, callback) => CucumberGiven(step, async function () {
|
|
54
|
+
await callback(this.testEnvironmentWorld);
|
|
55
|
+
}),
|
|
56
|
+
When: (step, callback) => CucumberWhen(step, async function () {
|
|
57
|
+
await callback(this.testEnvironmentWorld);
|
|
58
|
+
}),
|
|
59
|
+
Then: (step, callback) => CucumberThen(step, async function () {
|
|
60
|
+
await callback(this.testEnvironmentWorld);
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
};
|
package/guards.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { InteractorConfig, RunnerConfig, ServiceConfig } from './type.ts';
|
|
2
|
+
import type { FilterRecord, GetKey, RecordToEntries } from '@dungarees/core/type-util.ts';
|
|
3
|
+
export type InteractorNames<SERVICES extends Record<string, ServiceConfig>> = GetKey<RecordToEntries<FilterRecord<SERVICES, InteractorConfig>>>;
|
|
4
|
+
export type RunnerNames<SERVICES extends Record<string, ServiceConfig>> = GetKey<RecordToEntries<FilterRecord<SERVICES, RunnerConfig>>>;
|
|
5
|
+
export declare const isInteractorName: <SERVICES extends Record<string, ServiceConfig>>(serviceConfigs: SERVICES, name: string) => name is InteractorNames<SERVICES> & string;
|
|
6
|
+
export declare const isRunnerName: <SERVICES extends Record<string, ServiceConfig>>(serviceConfigs: SERVICES, name: string) => name is RunnerNames<SERVICES> & string;
|
package/guards.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Narrows the name, not the config. A guard on the config alone tells TypeScript nothing about
|
|
2
|
+
// which key it came from, so the interactor and runner maps — keyed by their own subset of the
|
|
3
|
+
// service names — cannot be written to without it.
|
|
4
|
+
export const isInteractorName = (serviceConfigs, name) => serviceConfigs[name]?.type === 'interactor';
|
|
5
|
+
export const isRunnerName = (serviceConfigs, name) => serviceConfigs[name]?.type === 'runner';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Interactor } from '@bobcats-coding/skid/test/type.js';
|
|
2
|
+
import type { APIRequestContext, BrowserContext, LaunchOptions, Page } from '@playwright/test';
|
|
3
|
+
export type BrowserInteractorContext = {
|
|
4
|
+
context: BrowserContext;
|
|
5
|
+
page: Page;
|
|
6
|
+
};
|
|
7
|
+
export type RequestInteractorContext = {
|
|
8
|
+
context: APIRequestContext;
|
|
9
|
+
};
|
|
10
|
+
export declare const VALID_BROWSERS: readonly ["firefox", "webkit", "chromium"];
|
|
11
|
+
export type BrowserTypes = (typeof VALID_BROWSERS)[number];
|
|
12
|
+
export type BrowserInteractorConfig = {
|
|
13
|
+
browserOptions: LaunchOptions;
|
|
14
|
+
browser: BrowserTypes;
|
|
15
|
+
tracesDir: string;
|
|
16
|
+
screenshotsDir: string;
|
|
17
|
+
isVideoEnabled: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare const browserInteractor: (config: BrowserInteractorConfig) => Interactor<BrowserInteractorContext>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { chromium, firefox, webkit } from '@playwright/test';
|
|
2
|
+
import { ensureDir } from 'fs-extra';
|
|
3
|
+
import { fromEventPattern, map } from 'rxjs';
|
|
4
|
+
export const VALID_BROWSERS = ['firefox', 'webkit', 'chromium'];
|
|
5
|
+
export const browserInteractor = (config) => {
|
|
6
|
+
const BROWSERS = {
|
|
7
|
+
firefox,
|
|
8
|
+
webkit,
|
|
9
|
+
chromium,
|
|
10
|
+
};
|
|
11
|
+
let browser;
|
|
12
|
+
return {
|
|
13
|
+
start: async () => {
|
|
14
|
+
browser = await (BROWSERS[config.browser] ?? chromium).launch(config.browserOptions);
|
|
15
|
+
await ensureDir(config.tracesDir);
|
|
16
|
+
},
|
|
17
|
+
stop: async () => {
|
|
18
|
+
await browser.close();
|
|
19
|
+
},
|
|
20
|
+
startContext: async () => {
|
|
21
|
+
const context = await browser.newContext({
|
|
22
|
+
acceptDownloads: true,
|
|
23
|
+
viewport: { width: 1200, height: 800 },
|
|
24
|
+
...(config.isVideoEnabled ? { recordVideo: { dir: 'reports/screenshots' } } : {}),
|
|
25
|
+
});
|
|
26
|
+
await context.tracing.start({ screenshots: true, snapshots: true });
|
|
27
|
+
const page = await context.newPage();
|
|
28
|
+
const console$ = fromEventPattern((handler) => page.on('console', (msg) => {
|
|
29
|
+
handler(`${msg.type()}: ${msg.text()}`);
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
context: { page, context },
|
|
33
|
+
reportEntry$: console$.pipe(map((log) => ({
|
|
34
|
+
entry: `console -> ${log}`,
|
|
35
|
+
type: 'text/plain',
|
|
36
|
+
}))),
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
stopContext: async ({ page, context }) => {
|
|
40
|
+
await page.close();
|
|
41
|
+
await context.close();
|
|
42
|
+
},
|
|
43
|
+
onFailure: async ({ page, context }, testName) => {
|
|
44
|
+
const image = await page.screenshot();
|
|
45
|
+
await context.tracing.stop({
|
|
46
|
+
path: `${config.tracesDir}/${testName}-${new Date().toISOString().split('.')[0] ?? ''}-trace.zip`,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
entry: image,
|
|
50
|
+
type: 'image/png',
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EMPTY } from 'rxjs';
|
|
2
|
+
import { Network } from 'testcontainers';
|
|
3
|
+
export const networkInteractor = () => {
|
|
4
|
+
const network = new Network();
|
|
5
|
+
let startedNetwork;
|
|
6
|
+
return {
|
|
7
|
+
start: async () => {
|
|
8
|
+
startedNetwork = await network.start();
|
|
9
|
+
},
|
|
10
|
+
stop: async () => {
|
|
11
|
+
if (startedNetwork !== undefined) {
|
|
12
|
+
await startedNetwork.stop();
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
startContext: async () => {
|
|
16
|
+
if (startedNetwork === undefined) {
|
|
17
|
+
throw new Error('Network not started');
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
context: {
|
|
21
|
+
network: startedNetwork,
|
|
22
|
+
},
|
|
23
|
+
reportEntry$: EMPTY,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
stopContext: async () => { },
|
|
27
|
+
onFailure: async () => {
|
|
28
|
+
return {
|
|
29
|
+
entry: 'Network failed to start',
|
|
30
|
+
type: 'text/plain',
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Interactor } from '../type.ts';
|
|
2
|
+
import type { StartedNetwork } from 'testcontainers';
|
|
3
|
+
export type CommandResult = {
|
|
4
|
+
output: string;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
};
|
|
7
|
+
export type ExecContext = {
|
|
8
|
+
user: string;
|
|
9
|
+
workingDir: string;
|
|
10
|
+
environment: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
export type NodeCommandLineContext = {
|
|
13
|
+
exec: (command: string, context?: Partial<ExecContext>) => Promise<CommandResult>;
|
|
14
|
+
execWithAssertions: (command: string, context?: Partial<ExecContext>) => Promise<CommandResult>;
|
|
15
|
+
};
|
|
16
|
+
export type NodeCommandLineConfig = {
|
|
17
|
+
workingDir: string;
|
|
18
|
+
environment?: Record<string, string>;
|
|
19
|
+
network?: StartedNetwork;
|
|
20
|
+
path?: string;
|
|
21
|
+
bindMount?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const nodeCommandLineInteractor: ({ workingDir, environment, network, path, bindMount, }: NodeCommandLineConfig) => Interactor<NodeCommandLineContext>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NODE_CONTAINER_IMAGE } from '../constants.js';
|
|
2
|
+
import { ReplaySubject } from 'rxjs';
|
|
3
|
+
import { GenericContainer } from 'testcontainers';
|
|
4
|
+
export const nodeCommandLineInteractor = ({ workingDir, environment = {}, network, path, bindMount, }) => {
|
|
5
|
+
let runningContainer;
|
|
6
|
+
const reportEntry$ = new ReplaySubject();
|
|
7
|
+
const container = new GenericContainer(NODE_CONTAINER_IMAGE)
|
|
8
|
+
.withWorkingDir(workingDir)
|
|
9
|
+
.withEnvironment(environment)
|
|
10
|
+
// Keep container running with tail -f
|
|
11
|
+
.withCommand(['tail', '-f', '/dev/null'])
|
|
12
|
+
.withLogConsumer((stream) => {
|
|
13
|
+
stream.on('data', (line) => {
|
|
14
|
+
console.log(line);
|
|
15
|
+
});
|
|
16
|
+
stream.on('err', (line) => {
|
|
17
|
+
console.error(line);
|
|
18
|
+
});
|
|
19
|
+
stream.on('end', () => {
|
|
20
|
+
console.log(`Container closed: ${NODE_CONTAINER_IMAGE}`);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
if (network !== undefined) {
|
|
24
|
+
container.withNetwork(network);
|
|
25
|
+
}
|
|
26
|
+
if (bindMount !== undefined) {
|
|
27
|
+
container.withBindMounts([
|
|
28
|
+
{
|
|
29
|
+
source: bindMount,
|
|
30
|
+
target: '/opt/app/',
|
|
31
|
+
mode: 'rw',
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
else if (path !== undefined) {
|
|
36
|
+
container.withCopyDirectoriesToContainer([
|
|
37
|
+
{
|
|
38
|
+
source: path,
|
|
39
|
+
target: '/opt/app/',
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
const createContext = (container) => {
|
|
44
|
+
const runCommand = async (command, context) => {
|
|
45
|
+
// Unfortunately, testcontainers does not support reading stderr
|
|
46
|
+
const redirectedCommand = `${command} 2>&1`;
|
|
47
|
+
const result = await container.exec(['sh', '-c', redirectedCommand], {
|
|
48
|
+
...(context?.user === undefined ? {} : { user: context.user }),
|
|
49
|
+
...(context?.workingDir === undefined ? {} : { workingDir: context.workingDir }),
|
|
50
|
+
env: { ...environment, ...context?.environment },
|
|
51
|
+
});
|
|
52
|
+
reportEntry$.next({
|
|
53
|
+
entry: `Command executed: ${command}\nExit code: ${result.exitCode}`,
|
|
54
|
+
type: 'text/plain',
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
exec: async (command, context) => {
|
|
60
|
+
return runCommand(command, context);
|
|
61
|
+
},
|
|
62
|
+
execWithAssertions: async (command, context) => {
|
|
63
|
+
const result = await runCommand(command, context);
|
|
64
|
+
if (result.exitCode !== 0) {
|
|
65
|
+
console.error(result.output);
|
|
66
|
+
throw new Error(`Command failed: ${command}\nExit code: ${result.exitCode}\nOutput: ${result.output}`);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
start: async () => {
|
|
74
|
+
runningContainer = await container.start();
|
|
75
|
+
},
|
|
76
|
+
stop: async () => {
|
|
77
|
+
if (runningContainer !== undefined) {
|
|
78
|
+
await runningContainer.stop();
|
|
79
|
+
console.log(`Container stopped: ${workingDir}`);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
startContext: async () => {
|
|
83
|
+
if (!runningContainer) {
|
|
84
|
+
throw new Error('Container not started');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
context: createContext(runningContainer),
|
|
88
|
+
reportEntry$: reportEntry$.asObservable(),
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
stopContext: async () => {
|
|
92
|
+
// Context cleanup handled in stop()
|
|
93
|
+
},
|
|
94
|
+
onFailure: async (testName) => ({
|
|
95
|
+
entry: `Node command line failed during test: ${testName}`,
|
|
96
|
+
type: 'text/plain',
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dungarees/test-environment",
|
|
3
|
+
"engines": {
|
|
4
|
+
"node": ">=25.0.0"
|
|
5
|
+
},
|
|
6
|
+
"scripts": {
|
|
7
|
+
"type-check": "tsc --noEmit"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@dungarees/core": "*",
|
|
12
|
+
"rxjs": "^7.8.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@bobcats-coding/skid": "*",
|
|
16
|
+
"@cucumber/cucumber": "^10.9.0",
|
|
17
|
+
"@playwright/test": "^1.54.1",
|
|
18
|
+
"@testcontainers/postgresql": "^10.28.0",
|
|
19
|
+
"fs-extra": "^11.3.0",
|
|
20
|
+
"testcontainers": "^10.28.0",
|
|
21
|
+
"vite": "^5.0.12",
|
|
22
|
+
"vitest": "^3.0.2"
|
|
23
|
+
},
|
|
24
|
+
"author": "info@productkind.com",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"version": "0.11.4",
|
|
27
|
+
"exports": {
|
|
28
|
+
"./constants.ts": {
|
|
29
|
+
"import": "./constants.js",
|
|
30
|
+
"types": "./constants.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./cucumber-test-environment.ts": {
|
|
33
|
+
"import": "./cucumber-test-environment.js",
|
|
34
|
+
"types": "./cucumber-test-environment.d.ts"
|
|
35
|
+
},
|
|
36
|
+
"./guards.ts": {
|
|
37
|
+
"import": "./guards.js",
|
|
38
|
+
"types": "./guards.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"./interactors/browser.ts": {
|
|
41
|
+
"import": "./interactors/browser.js",
|
|
42
|
+
"types": "./interactors/browser.d.ts"
|
|
43
|
+
},
|
|
44
|
+
"./interactors/network.ts": {
|
|
45
|
+
"import": "./interactors/network.js",
|
|
46
|
+
"types": "./interactors/network.d.ts"
|
|
47
|
+
},
|
|
48
|
+
"./interactors/node-command-line.ts": {
|
|
49
|
+
"import": "./interactors/node-command-line.js",
|
|
50
|
+
"types": "./interactors/node-command-line.d.ts"
|
|
51
|
+
},
|
|
52
|
+
"./runners/dev-server.ts": {
|
|
53
|
+
"import": "./runners/dev-server.js",
|
|
54
|
+
"types": "./runners/dev-server.d.ts"
|
|
55
|
+
},
|
|
56
|
+
"./runners/node-process.ts": {
|
|
57
|
+
"import": "./runners/node-process.js",
|
|
58
|
+
"types": "./runners/node-process.d.ts"
|
|
59
|
+
},
|
|
60
|
+
"./runners/npm-registry.ts": {
|
|
61
|
+
"import": "./runners/npm-registry.js",
|
|
62
|
+
"types": "./runners/npm-registry.d.ts"
|
|
63
|
+
},
|
|
64
|
+
"./runners/postgres.ts": {
|
|
65
|
+
"import": "./runners/postgres.js",
|
|
66
|
+
"types": "./runners/postgres.d.ts"
|
|
67
|
+
},
|
|
68
|
+
"./test-environment.test.ts": {
|
|
69
|
+
"import": "./test-environment.test.js",
|
|
70
|
+
"types": "./test-environment.test.d.ts"
|
|
71
|
+
},
|
|
72
|
+
"./test-environment.ts": {
|
|
73
|
+
"import": "./test-environment.js",
|
|
74
|
+
"types": "./test-environment.d.ts"
|
|
75
|
+
},
|
|
76
|
+
"./type.ts": {
|
|
77
|
+
"import": "./type.js",
|
|
78
|
+
"types": "./type.d.ts"
|
|
79
|
+
},
|
|
80
|
+
"./world.ts": {
|
|
81
|
+
"import": "./world.js",
|
|
82
|
+
"types": "./world.d.ts"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createServer } from 'vite';
|
|
2
|
+
export const devServerRunner = (config) => {
|
|
3
|
+
let viteServer;
|
|
4
|
+
return {
|
|
5
|
+
start: async () => {
|
|
6
|
+
viteServer = await createServer({
|
|
7
|
+
root: config.rootDir,
|
|
8
|
+
server: {
|
|
9
|
+
port: config.port,
|
|
10
|
+
},
|
|
11
|
+
logLevel: 'error',
|
|
12
|
+
});
|
|
13
|
+
await viteServer.listen();
|
|
14
|
+
},
|
|
15
|
+
stop: async () => {
|
|
16
|
+
await viteServer.close();
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Runner } from '@bobcats-coding/skid/test/type.js';
|
|
2
|
+
import type { StartedNetwork } from 'testcontainers';
|
|
3
|
+
export type NodeProcessRunnerConfig = {
|
|
4
|
+
path: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
port: number;
|
|
7
|
+
environment?: Record<string, string>;
|
|
8
|
+
network?: StartedNetwork;
|
|
9
|
+
};
|
|
10
|
+
export declare const nodeProcessRunner: ({ path, args, port, environment, network, }: NodeProcessRunnerConfig) => Runner;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { GenericContainer, Wait } from 'testcontainers';
|
|
2
|
+
export const nodeProcessRunner = ({ path, args, port, environment = {}, network, }) => {
|
|
3
|
+
let runningContainer;
|
|
4
|
+
const container = new GenericContainer('node:24-bullseye')
|
|
5
|
+
.withWorkingDir('/opt/app')
|
|
6
|
+
.withEnvironment(environment)
|
|
7
|
+
.withCopyDirectoriesToContainer([
|
|
8
|
+
{
|
|
9
|
+
source: path,
|
|
10
|
+
target: '/opt/app/',
|
|
11
|
+
},
|
|
12
|
+
])
|
|
13
|
+
.withExposedPorts({
|
|
14
|
+
container: port,
|
|
15
|
+
host: port,
|
|
16
|
+
})
|
|
17
|
+
.withEntrypoint(args)
|
|
18
|
+
.withLogConsumer((stream) => {
|
|
19
|
+
stream.on('data', (line) => {
|
|
20
|
+
console.log(line);
|
|
21
|
+
});
|
|
22
|
+
stream.on('err', (line) => {
|
|
23
|
+
console.error(line);
|
|
24
|
+
});
|
|
25
|
+
stream.on('end', () => {
|
|
26
|
+
console.log(`Container closed: ${path} ${args.join(' ')}}`);
|
|
27
|
+
});
|
|
28
|
+
})
|
|
29
|
+
.withWaitStrategy(Wait.forListeningPorts().withStartupTimeout(300000));
|
|
30
|
+
if (network !== undefined) {
|
|
31
|
+
container.withNetwork(network);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
start: async () => {
|
|
35
|
+
runningContainer = await container.start();
|
|
36
|
+
},
|
|
37
|
+
stop: async () => {
|
|
38
|
+
if (runningContainer !== undefined) {
|
|
39
|
+
await runningContainer.stop();
|
|
40
|
+
console.log(`Container stopped: ${path} ${args.join(' ')}}`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Runner } from '../type.ts';
|
|
2
|
+
import type { StartedNetwork } from 'testcontainers';
|
|
3
|
+
export type NpmRegistryRunnerConfig = {
|
|
4
|
+
port: number;
|
|
5
|
+
environment?: Record<string, string>;
|
|
6
|
+
network?: StartedNetwork;
|
|
7
|
+
alias?: string;
|
|
8
|
+
localScopes?: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare const npmRegistryRunner: ({ port, environment, network, alias, localScopes, }: NpmRegistryRunnerConfig) => Runner;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { GenericContainer, Wait } from 'testcontainers';
|
|
2
|
+
const generateVerdaccioConfig = (localScopes) => {
|
|
3
|
+
const localScopeRules = localScopes
|
|
4
|
+
.map((scope) => ` '${scope}/*':\n access: $all\n publish: $authenticated`)
|
|
5
|
+
.join('\n');
|
|
6
|
+
return `storage: /verdaccio/storage/data
|
|
7
|
+
auth:
|
|
8
|
+
htpasswd:
|
|
9
|
+
file: /verdaccio/storage/htpasswd
|
|
10
|
+
uplinks:
|
|
11
|
+
npmjs:
|
|
12
|
+
url: https://registry.npmjs.org/
|
|
13
|
+
packages:
|
|
14
|
+
${localScopeRules}
|
|
15
|
+
'@*/*':
|
|
16
|
+
access: $all
|
|
17
|
+
publish: $authenticated
|
|
18
|
+
proxy: npmjs
|
|
19
|
+
'**':
|
|
20
|
+
access: $all
|
|
21
|
+
publish: $authenticated
|
|
22
|
+
proxy: npmjs
|
|
23
|
+
`;
|
|
24
|
+
};
|
|
25
|
+
export const npmRegistryRunner = ({ port, environment = {}, network, alias, localScopes, }) => {
|
|
26
|
+
let runningContainer;
|
|
27
|
+
const container = new GenericContainer('verdaccio/verdaccio')
|
|
28
|
+
.withExposedPorts({
|
|
29
|
+
container: port,
|
|
30
|
+
host: port,
|
|
31
|
+
})
|
|
32
|
+
.withEnvironment(environment)
|
|
33
|
+
.withLogConsumer((stream) => {
|
|
34
|
+
stream.on('data', (line) => {
|
|
35
|
+
console.log(line);
|
|
36
|
+
});
|
|
37
|
+
stream.on('err', (line) => {
|
|
38
|
+
console.error(line);
|
|
39
|
+
});
|
|
40
|
+
stream.on('end', () => {
|
|
41
|
+
console.log('Verdaccio container closed');
|
|
42
|
+
});
|
|
43
|
+
})
|
|
44
|
+
.withWaitStrategy(Wait.forListeningPorts().withStartupTimeout(300000));
|
|
45
|
+
if (localScopes !== undefined) {
|
|
46
|
+
container.withCopyContentToContainer([
|
|
47
|
+
{
|
|
48
|
+
content: generateVerdaccioConfig(localScopes),
|
|
49
|
+
target: '/verdaccio/conf/config.yaml',
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
if (alias !== undefined) {
|
|
54
|
+
container.withName(alias);
|
|
55
|
+
}
|
|
56
|
+
if (network !== undefined) {
|
|
57
|
+
container.withNetwork(network);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
start: async () => {
|
|
61
|
+
runningContainer = await container.start();
|
|
62
|
+
},
|
|
63
|
+
stop: async () => {
|
|
64
|
+
if (runningContainer !== undefined) {
|
|
65
|
+
await runningContainer.stop();
|
|
66
|
+
console.log(`Verdaccio container stopped: ${alias ?? 'npm-registry'}`);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Runner } from '@bobcats-coding/skid/test/type.js';
|
|
2
|
+
import type { StartedNetwork } from 'testcontainers';
|
|
3
|
+
export type PostgresRunnerConfig = {
|
|
4
|
+
port: number;
|
|
5
|
+
environment?: Record<string, string>;
|
|
6
|
+
network?: StartedNetwork;
|
|
7
|
+
};
|
|
8
|
+
export declare const postgresRunner: ({ port, environment, network, }: PostgresRunnerConfig) => Runner;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
|
2
|
+
import { Wait } from 'testcontainers';
|
|
3
|
+
export const postgresRunner = ({ port, environment = {}, network, }) => {
|
|
4
|
+
let runningContainer;
|
|
5
|
+
const container = new PostgreSqlContainer()
|
|
6
|
+
.withEnvironment(environment)
|
|
7
|
+
.withExposedPorts({
|
|
8
|
+
container: port,
|
|
9
|
+
host: port,
|
|
10
|
+
})
|
|
11
|
+
.withLogConsumer((stream) => {
|
|
12
|
+
stream.on('data', (line) => {
|
|
13
|
+
console.log(line);
|
|
14
|
+
});
|
|
15
|
+
stream.on('err', (line) => {
|
|
16
|
+
console.error(line);
|
|
17
|
+
});
|
|
18
|
+
stream.on('end', () => {
|
|
19
|
+
console.log('Container closed');
|
|
20
|
+
});
|
|
21
|
+
})
|
|
22
|
+
.withWaitStrategy(Wait.forListeningPorts().withStartupTimeout(300000));
|
|
23
|
+
if (network !== undefined) {
|
|
24
|
+
container.withNetwork(network).withNetworkAliases('postgres');
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
start: async () => {
|
|
28
|
+
runningContainer = await container.start();
|
|
29
|
+
},
|
|
30
|
+
stop: async () => {
|
|
31
|
+
if (runningContainer !== undefined) {
|
|
32
|
+
await runningContainer.stop();
|
|
33
|
+
console.log('Container stopped');
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DefaultConfig, Interactor, ReportEntry, Runner, ServiceConfig } from './type.ts';
|
|
2
|
+
import type { TestEnvironmentWorld } from './world.ts';
|
|
3
|
+
import type { Observable } from 'rxjs';
|
|
4
|
+
export type TestEnviornment<SERVICES extends Record<string, ServiceConfig>> = {
|
|
5
|
+
onBeforeAll: () => Promise<Observable<ReportEntry>>;
|
|
6
|
+
onAfterAll: () => Promise<Observable<ReportEntry>>;
|
|
7
|
+
onBefore: (world: TestEnvironmentWorld<SERVICES>) => Promise<Observable<ReportEntry>>;
|
|
8
|
+
onAfter: (world: TestEnvironmentWorld<SERVICES>) => Promise<Observable<ReportEntry>>;
|
|
9
|
+
onFailure: (world: TestEnvironmentWorld<SERVICES>, testName: string) => Promise<ReportEntry[]>;
|
|
10
|
+
createWorld: () => TestEnvironmentWorld<SERVICES>;
|
|
11
|
+
};
|
|
12
|
+
export declare const instantiateService: <ARGS extends unknown[], INSTANCE extends Interactor | Runner>({ creator, hook }: {
|
|
13
|
+
creator: (...args: ARGS) => INSTANCE | Promise<INSTANCE>;
|
|
14
|
+
} & DefaultConfig, ...args: ARGS) => Promise<{
|
|
15
|
+
instance: INSTANCE;
|
|
16
|
+
} & DefaultConfig>;
|
|
17
|
+
export declare const createTestEnvironment: <const SERVICES extends Record<string, ServiceConfig>>(serviceConfigs: SERVICES) => TestEnviornment<SERVICES>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { isInteractorName, isRunnerName } from './guards.js';
|
|
2
|
+
import { createWorld } from './world.js';
|
|
3
|
+
import { map, merge, ReplaySubject } from 'rxjs';
|
|
4
|
+
export const instantiateService = async ({ creator, hook }, ...args) => {
|
|
5
|
+
return {
|
|
6
|
+
instance: await creator(...args),
|
|
7
|
+
...(hook === undefined ? {} : { hook }),
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export const createTestEnvironment = (serviceConfigs) => {
|
|
11
|
+
const state = {
|
|
12
|
+
serviceConfigs,
|
|
13
|
+
interactors: new Map(),
|
|
14
|
+
runners: new Map(),
|
|
15
|
+
};
|
|
16
|
+
const isBeforeAll = ({ hook }) => hook === 'before-all';
|
|
17
|
+
const isNotBeforeAll = ({ hook }) => hook !== 'before-all';
|
|
18
|
+
const isBefore = ({ hook }) => hook === 'before';
|
|
19
|
+
const keyValueToObject = ([name, service]) => ({
|
|
20
|
+
name,
|
|
21
|
+
...service,
|
|
22
|
+
});
|
|
23
|
+
const asyncTransform = async (iterable, transform) => await Promise.all(transform([...iterable]));
|
|
24
|
+
// The two maps are keyed by different subsets of the service names, so iterating them
|
|
25
|
+
// separately keeps each key correlated with its own instance type; merging them first
|
|
26
|
+
// collapses both to a union and loses that.
|
|
27
|
+
const forEachService = async ({ hasHook, mapper, }) => {
|
|
28
|
+
await Promise.all([
|
|
29
|
+
...[...state.runners.entries()].map(keyValueToObject).filter(hasHook).map(mapper),
|
|
30
|
+
...[...state.interactors.entries()].map(keyValueToObject).filter(hasHook).map(mapper),
|
|
31
|
+
]);
|
|
32
|
+
};
|
|
33
|
+
const forEachBeforeAllService = async (mapper) => await forEachService({ hasHook: isBeforeAll, mapper });
|
|
34
|
+
const forEachScenarioService = async (mapper) => await forEachService({ hasHook: isNotBeforeAll, mapper });
|
|
35
|
+
const forEachBeforeService = async (mapper) => {
|
|
36
|
+
await asyncTransform([...state.interactors.values(), ...state.runners.values()], (list) => list.filter(isBefore).map(mapper));
|
|
37
|
+
};
|
|
38
|
+
const mapInteractors = async (mapper) => await asyncTransform(state.interactors.entries(), (list) => list.map(async ([name, interactor]) => await mapper({ name, ...interactor })));
|
|
39
|
+
const instantiateAll = async (filter) => {
|
|
40
|
+
await Promise.all(Object.entries(serviceConfigs)
|
|
41
|
+
.filter(([_, config]) => filter(config))
|
|
42
|
+
.map(async ([key, service]) => {
|
|
43
|
+
if (service.type === 'interactor' && isInteractorName(serviceConfigs, key)) {
|
|
44
|
+
state.interactors.set(key, await instantiateService(service));
|
|
45
|
+
}
|
|
46
|
+
if (service.type === 'runner' && isRunnerName(serviceConfigs, key)) {
|
|
47
|
+
state.runners.set(key, await instantiateService(service));
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
onBeforeAll: async () => {
|
|
53
|
+
const entries$ = new ReplaySubject();
|
|
54
|
+
await instantiateAll(isBeforeAll);
|
|
55
|
+
await forEachBeforeAllService(async ({ instance, name }) => {
|
|
56
|
+
await instance.start();
|
|
57
|
+
const message = `${name}: Started in before-all`;
|
|
58
|
+
entries$.next({ entry: message, type: 'text/plain' });
|
|
59
|
+
console.log(message);
|
|
60
|
+
});
|
|
61
|
+
return entries$.asObservable();
|
|
62
|
+
},
|
|
63
|
+
onAfterAll: async () => {
|
|
64
|
+
const entries$ = new ReplaySubject();
|
|
65
|
+
await forEachBeforeAllService(async ({ instance, name }) => {
|
|
66
|
+
await instance.stop();
|
|
67
|
+
const message = `${name}: Stopped in after-all`;
|
|
68
|
+
entries$.next({ entry: message, type: 'text/plain' });
|
|
69
|
+
console.log(message);
|
|
70
|
+
});
|
|
71
|
+
return entries$.asObservable();
|
|
72
|
+
},
|
|
73
|
+
onBefore: async (world) => {
|
|
74
|
+
await instantiateAll(isBefore);
|
|
75
|
+
await forEachBeforeService(async ({ instance }) => {
|
|
76
|
+
await instance.start();
|
|
77
|
+
});
|
|
78
|
+
const reportEntries = await mapInteractors(async ({ name, instance }) => {
|
|
79
|
+
const { context, reportEntry$ } = await instance.startContext();
|
|
80
|
+
world.register(name, context);
|
|
81
|
+
return reportEntry$.pipe(map(({ entry, type }) => type === 'text/plain' ? { type, entry: `${String(name)}: ${entry}` } : { type, entry }));
|
|
82
|
+
});
|
|
83
|
+
return merge(...reportEntries);
|
|
84
|
+
},
|
|
85
|
+
onAfter: async () => {
|
|
86
|
+
const entries$ = new ReplaySubject();
|
|
87
|
+
await mapInteractors(async ({ instance }) => {
|
|
88
|
+
await instance.stopContext();
|
|
89
|
+
});
|
|
90
|
+
await forEachScenarioService(async ({ instance, name }) => {
|
|
91
|
+
await instance.stop();
|
|
92
|
+
const message = `${name}: Stopped in after`;
|
|
93
|
+
entries$.next({ entry: message, type: 'text/plain' });
|
|
94
|
+
});
|
|
95
|
+
return entries$.asObservable();
|
|
96
|
+
},
|
|
97
|
+
onFailure: async (_world, testName) => await mapInteractors(async ({ instance }) => await instance.onFailure(testName)),
|
|
98
|
+
createWorld: () => createWorld(state),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { createTestEnvironment } from './test-environment.js';
|
|
2
|
+
import { of } from 'rxjs';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
test('onBeforeAll hook starts the runners', async () => {
|
|
5
|
+
const { state: state1, creator: creator1 } = setupFakeRunner();
|
|
6
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
7
|
+
const testEnvironment = createTestEnvironment({
|
|
8
|
+
service1: { type: 'runner', creator: creator1, hook: 'before-all' },
|
|
9
|
+
service2: { type: 'runner', creator: creator2, hook: 'before-all' },
|
|
10
|
+
});
|
|
11
|
+
await testEnvironment.onBeforeAll();
|
|
12
|
+
expect(state1.isStarted).toBe(true);
|
|
13
|
+
expect(state2.isStarted).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
test('onBeforeAll hook starts the interactors', async () => {
|
|
16
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
17
|
+
const { state: state2, creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
18
|
+
const testEnvironment = createTestEnvironment({
|
|
19
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before-all' },
|
|
20
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
21
|
+
});
|
|
22
|
+
await testEnvironment.onBeforeAll();
|
|
23
|
+
expect(state1.isStarted).toBe(true);
|
|
24
|
+
expect(state2.isStarted).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
test('onAfterAll hook stops the runners', async () => {
|
|
27
|
+
const { state: state1, creator: creator1 } = setupFakeRunner();
|
|
28
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
29
|
+
const testEnvironment = createTestEnvironment({
|
|
30
|
+
service1: { type: 'runner', creator: creator1, hook: 'before-all' },
|
|
31
|
+
service2: { type: 'runner', creator: creator2, hook: 'before-all' },
|
|
32
|
+
});
|
|
33
|
+
await testEnvironment.onBeforeAll();
|
|
34
|
+
await testEnvironment.onAfterAll();
|
|
35
|
+
expect(state1.isStarted).toBe(false);
|
|
36
|
+
expect(state2.isStarted).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
test('onAfterAll hook stops the interactors', async () => {
|
|
39
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
40
|
+
const { state: state2, creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
41
|
+
const testEnvironment = createTestEnvironment({
|
|
42
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before-all' },
|
|
43
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
44
|
+
});
|
|
45
|
+
await testEnvironment.onBeforeAll();
|
|
46
|
+
await testEnvironment.onAfterAll();
|
|
47
|
+
expect(state1.isStarted).toBe(false);
|
|
48
|
+
expect(state2.isStarted).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
test('onBefore hook starts the contexts', async () => {
|
|
51
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
52
|
+
const { state: state2, creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
53
|
+
const testEnvironment = createTestEnvironment({
|
|
54
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before-all' },
|
|
55
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
56
|
+
});
|
|
57
|
+
await testEnvironment.onBeforeAll();
|
|
58
|
+
const world = testEnvironment.createWorld();
|
|
59
|
+
await testEnvironment.onBefore(world);
|
|
60
|
+
expect(state1.isContextStarted).toBe(true);
|
|
61
|
+
expect(state2.isContextStarted).toBe(true);
|
|
62
|
+
expect(world.get('service1')).toBe(1);
|
|
63
|
+
expect(world.get('service2')).toBe(2);
|
|
64
|
+
});
|
|
65
|
+
test('world keys should be typesafe', async () => {
|
|
66
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
67
|
+
const { creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
68
|
+
const testEnvironment = createTestEnvironment({
|
|
69
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before-all' },
|
|
70
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
71
|
+
});
|
|
72
|
+
await testEnvironment.onBeforeAll();
|
|
73
|
+
const world = testEnvironment.createWorld();
|
|
74
|
+
await testEnvironment.onBefore(world);
|
|
75
|
+
// @ts-expect-error service3 is not in the configuration
|
|
76
|
+
expect(() => world.get('service3')).toThrow();
|
|
77
|
+
const a = world.get('service1');
|
|
78
|
+
expect(a).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
test('onBefore hook starts the "before" services', async () => {
|
|
81
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
82
|
+
const { state: state2, creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
83
|
+
const testEnvironment = createTestEnvironment({
|
|
84
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before' },
|
|
85
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
86
|
+
});
|
|
87
|
+
const world = testEnvironment.createWorld();
|
|
88
|
+
await testEnvironment.onBefore(world);
|
|
89
|
+
expect(state1.isStarted).toBe(true);
|
|
90
|
+
expect(state2.isStarted).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
test('onBeforeAll hook only starts the "before-all" services', async () => {
|
|
93
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
94
|
+
const { state: state2, creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
95
|
+
const testEnvironment = createTestEnvironment({
|
|
96
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before' },
|
|
97
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
98
|
+
});
|
|
99
|
+
await testEnvironment.onBeforeAll();
|
|
100
|
+
expect(state1.isStarted).toBe(false);
|
|
101
|
+
expect(state2.isStarted).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
test('onAfter hook stops the "before" services', async () => {
|
|
104
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
105
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
106
|
+
const { state: state3, creator: creator3 } = setupFakeInteractor({ context: 2 });
|
|
107
|
+
const testEnvironment = createTestEnvironment({
|
|
108
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before' },
|
|
109
|
+
service2: { type: 'runner', creator: creator2, hook: 'before' },
|
|
110
|
+
service3: { type: 'interactor', creator: creator3, hook: 'before-all' },
|
|
111
|
+
});
|
|
112
|
+
await testEnvironment.onBeforeAll();
|
|
113
|
+
const world = testEnvironment.createWorld();
|
|
114
|
+
await testEnvironment.onBefore(world);
|
|
115
|
+
await testEnvironment.onAfter(world);
|
|
116
|
+
expect(state1.isStarted).toBe(false);
|
|
117
|
+
expect(state2.isStarted).toBe(false);
|
|
118
|
+
expect(state3.isStarted).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
test('onAfterAll hook only stops the "before-all" services', async () => {
|
|
121
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
122
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
123
|
+
const { state: state3, creator: creator3 } = setupFakeInteractor({ context: 2 });
|
|
124
|
+
const { state: state4, creator: creator4 } = setupFakeRunner();
|
|
125
|
+
const testEnvironment = createTestEnvironment({
|
|
126
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before' },
|
|
127
|
+
service2: { type: 'runner', creator: creator2, hook: 'before' },
|
|
128
|
+
service3: { type: 'interactor', creator: creator3, hook: 'before-all' },
|
|
129
|
+
service4: { type: 'runner', creator: creator4, hook: 'before-all' },
|
|
130
|
+
});
|
|
131
|
+
await testEnvironment.onBeforeAll();
|
|
132
|
+
const world = testEnvironment.createWorld();
|
|
133
|
+
await testEnvironment.onBefore(world);
|
|
134
|
+
await testEnvironment.onAfterAll();
|
|
135
|
+
expect(state1.isStarted).toBe(true);
|
|
136
|
+
expect(state2.isStarted).toBe(true);
|
|
137
|
+
expect(state3.isStarted).toBe(false);
|
|
138
|
+
expect(state4.isStarted).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
test('start service from the world', async () => {
|
|
141
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
142
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
143
|
+
const { state: state3, creator: creator3 } = setupFakeInteractor({ context: 3 });
|
|
144
|
+
const testEnvironment = createTestEnvironment({
|
|
145
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
146
|
+
service2: { type: 'runner', creator: creator2 },
|
|
147
|
+
service3: { type: 'interactor', creator: creator3, hook: 'before-all' },
|
|
148
|
+
});
|
|
149
|
+
const world = testEnvironment.createWorld();
|
|
150
|
+
expect(state1.isStarted).toBe(false);
|
|
151
|
+
expect(state2.isStarted).toBe(false);
|
|
152
|
+
expect(state3.isStarted).toBe(false);
|
|
153
|
+
await world.start('service1');
|
|
154
|
+
await world.start('service2');
|
|
155
|
+
expect(state1.isStarted).toBe(true);
|
|
156
|
+
expect(state2.isStarted).toBe(true);
|
|
157
|
+
expect(state3.isStarted).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
test('world should be able to get the started service', async () => {
|
|
160
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
161
|
+
const { state: state2, creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
162
|
+
const testEnvironment = createTestEnvironment({
|
|
163
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
164
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
165
|
+
});
|
|
166
|
+
const world = testEnvironment.createWorld();
|
|
167
|
+
expect(state1.isStarted).toBe(false);
|
|
168
|
+
expect(state2.isStarted).toBe(false);
|
|
169
|
+
await world.start('service1');
|
|
170
|
+
const context = world.get('service1');
|
|
171
|
+
expect(context).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
test('world should be able to pass params when starts a service', async () => {
|
|
174
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
175
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
176
|
+
const testEnvironment = createTestEnvironment({
|
|
177
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
178
|
+
service2: { type: 'runner', creator: creator2 },
|
|
179
|
+
});
|
|
180
|
+
const world = testEnvironment.createWorld();
|
|
181
|
+
await world.start('service1', 2);
|
|
182
|
+
await world.start('service2', 3);
|
|
183
|
+
expect(state1.startArg).toBe(2);
|
|
184
|
+
expect(state2.startArg).toBe(3);
|
|
185
|
+
});
|
|
186
|
+
test('should not start non hook services', async () => {
|
|
187
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
188
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
189
|
+
const testEnvironment = createTestEnvironment({
|
|
190
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
191
|
+
service2: { type: 'runner', creator: creator2 },
|
|
192
|
+
});
|
|
193
|
+
const world = testEnvironment.createWorld();
|
|
194
|
+
await testEnvironment.onBeforeAll();
|
|
195
|
+
await testEnvironment.onBefore(world);
|
|
196
|
+
expect(state1.isStarted).toBe(false);
|
|
197
|
+
expect(state2.isStarted).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
test('onAfter stops services started from the world', async () => {
|
|
200
|
+
const { state: state1, creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
201
|
+
const { state: state2, creator: creator2 } = setupFakeRunner();
|
|
202
|
+
const { state: state3, creator: creator3 } = setupFakeInteractor({ context: 2 });
|
|
203
|
+
const testEnvironment = createTestEnvironment({
|
|
204
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
205
|
+
service2: { type: 'runner', creator: creator2 },
|
|
206
|
+
service3: { type: 'interactor', creator: creator3, hook: 'before-all' },
|
|
207
|
+
});
|
|
208
|
+
const world = testEnvironment.createWorld();
|
|
209
|
+
await testEnvironment.onBefore(world);
|
|
210
|
+
await world.start('service1');
|
|
211
|
+
await world.start('service2');
|
|
212
|
+
await testEnvironment.onAfter(world);
|
|
213
|
+
expect(state1.isStarted).toBe(false);
|
|
214
|
+
expect(state2.isStarted).toBe(false);
|
|
215
|
+
expect(state3.isStarted).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
test('start should be type-safe', async () => {
|
|
218
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
219
|
+
const { creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
220
|
+
const testEnvironment = createTestEnvironment({
|
|
221
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
222
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
223
|
+
});
|
|
224
|
+
const world = testEnvironment.createWorld();
|
|
225
|
+
await expect(async () => {
|
|
226
|
+
// @ts-expect-error service3 is not in the configuration
|
|
227
|
+
await world.start('service3');
|
|
228
|
+
}).rejects.toThrow('Service "service3" is not in the configuration');
|
|
229
|
+
});
|
|
230
|
+
test('start is typesafe', async () => {
|
|
231
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
232
|
+
const { creator: creator2 } = setupFakeInteractor({ context: 2 });
|
|
233
|
+
const testEnvironment = createTestEnvironment({
|
|
234
|
+
service1: { type: 'interactor', creator: creator1 },
|
|
235
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
236
|
+
});
|
|
237
|
+
const world = testEnvironment.createWorld();
|
|
238
|
+
await expect(async () => {
|
|
239
|
+
// @ts-expect-error service3 is not in the configuration
|
|
240
|
+
await world.start('service3');
|
|
241
|
+
}).rejects.toThrow('Service "service3" is not in the configuration');
|
|
242
|
+
});
|
|
243
|
+
test('start context shows its log entries', async () => {
|
|
244
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
245
|
+
const { creator: creator2 } = setupFakeRunner();
|
|
246
|
+
const testEnvironment = createTestEnvironment({
|
|
247
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before' },
|
|
248
|
+
service2: { type: 'runner', creator: creator2, hook: 'before' },
|
|
249
|
+
});
|
|
250
|
+
const world = testEnvironment.createWorld();
|
|
251
|
+
const entries$ = await testEnvironment.onBefore(world);
|
|
252
|
+
const messages = new Set();
|
|
253
|
+
entries$.subscribe(({ entry }) => {
|
|
254
|
+
messages.add(entry);
|
|
255
|
+
});
|
|
256
|
+
expect(messages).toEqual(new Set(['service1: Coming from context start']));
|
|
257
|
+
});
|
|
258
|
+
test('onBeforeAll should log the started services', async () => {
|
|
259
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
260
|
+
const { creator: creator2 } = setupFakeRunner();
|
|
261
|
+
const testEnvironment = createTestEnvironment({
|
|
262
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before-all' },
|
|
263
|
+
service2: { type: 'runner', creator: creator2, hook: 'before-all' },
|
|
264
|
+
});
|
|
265
|
+
const entries$ = await testEnvironment.onBeforeAll();
|
|
266
|
+
const messages = new Set();
|
|
267
|
+
entries$.subscribe(({ entry }) => {
|
|
268
|
+
messages.add(entry);
|
|
269
|
+
});
|
|
270
|
+
expect(messages).toEqual(new Set(['service1: Started in before-all', 'service2: Started in before-all']));
|
|
271
|
+
});
|
|
272
|
+
test('onAfterAll should log the stopped services', async () => {
|
|
273
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
274
|
+
const { creator: creator2 } = setupFakeRunner();
|
|
275
|
+
const testEnvironment = createTestEnvironment({
|
|
276
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before-all' },
|
|
277
|
+
service2: { type: 'runner', creator: creator2, hook: 'before-all' },
|
|
278
|
+
});
|
|
279
|
+
await testEnvironment.onBeforeAll();
|
|
280
|
+
const entries$ = await testEnvironment.onAfterAll();
|
|
281
|
+
const messages = new Set();
|
|
282
|
+
entries$.subscribe(({ entry }) => {
|
|
283
|
+
messages.add(entry);
|
|
284
|
+
});
|
|
285
|
+
expect(messages).toEqual(new Set(['service1: Stopped in after-all', 'service2: Stopped in after-all']));
|
|
286
|
+
});
|
|
287
|
+
test('onAfter should log the stopped services', async () => {
|
|
288
|
+
const { creator: creator1 } = setupFakeInteractor({ context: 1 });
|
|
289
|
+
const { creator: creator2 } = setupFakeRunner();
|
|
290
|
+
const testEnvironment = createTestEnvironment({
|
|
291
|
+
service1: { type: 'interactor', creator: creator1, hook: 'before' },
|
|
292
|
+
service2: { type: 'runner', creator: creator2, hook: 'before' },
|
|
293
|
+
});
|
|
294
|
+
const world = testEnvironment.createWorld();
|
|
295
|
+
await testEnvironment.onBefore(world);
|
|
296
|
+
const entries$ = await testEnvironment.onAfter(world);
|
|
297
|
+
const messages = new Set();
|
|
298
|
+
entries$.subscribe(({ entry }) => {
|
|
299
|
+
messages.add(entry);
|
|
300
|
+
});
|
|
301
|
+
expect(messages).toEqual(new Set(['service1: Stopped in after', 'service2: Stopped in after']));
|
|
302
|
+
});
|
|
303
|
+
test('it should handle an async creator', async () => {
|
|
304
|
+
const { creator: creator1, state: state1 } = setupFakeAsyncRunner();
|
|
305
|
+
const { creator: creator2, state: state2 } = setupFakeAsyncInteractor({ context: 1 });
|
|
306
|
+
const testEnvironment = createTestEnvironment({
|
|
307
|
+
service1: { type: 'runner', creator: creator1 },
|
|
308
|
+
service2: { type: 'interactor', creator: creator2 },
|
|
309
|
+
});
|
|
310
|
+
const world = testEnvironment.createWorld();
|
|
311
|
+
await world.start('service1');
|
|
312
|
+
await world.start('service2');
|
|
313
|
+
expect(state1.isStarted).toBe(true);
|
|
314
|
+
expect(state2.isStarted).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
test('it should handle an async creator', async () => {
|
|
317
|
+
const { creator: creator1, state: state1 } = setupFakeAsyncRunner();
|
|
318
|
+
const { creator: creator2, state: state2 } = setupFakeAsyncInteractor({ context: 1 });
|
|
319
|
+
const testEnvironment = createTestEnvironment({
|
|
320
|
+
service1: { type: 'runner', creator: creator1, hook: 'before-all' },
|
|
321
|
+
service2: { type: 'interactor', creator: creator2, hook: 'before-all' },
|
|
322
|
+
});
|
|
323
|
+
await testEnvironment.onBeforeAll();
|
|
324
|
+
expect(state1.isStarted).toBe(true);
|
|
325
|
+
expect(state2.isStarted).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
const setupFakeInteractor = ({ context, isContextStarted, }) => {
|
|
328
|
+
const state = {
|
|
329
|
+
isStarted: false,
|
|
330
|
+
isContextStarted: isContextStarted ?? false,
|
|
331
|
+
startArg: undefined,
|
|
332
|
+
};
|
|
333
|
+
const creator = (arg) => ({
|
|
334
|
+
start: async () => {
|
|
335
|
+
state.isStarted = true;
|
|
336
|
+
state.startArg = arg;
|
|
337
|
+
},
|
|
338
|
+
stop: async () => {
|
|
339
|
+
state.isStarted = false;
|
|
340
|
+
},
|
|
341
|
+
startContext: async () => {
|
|
342
|
+
state.isContextStarted = true;
|
|
343
|
+
return {
|
|
344
|
+
context,
|
|
345
|
+
reportEntry$: of({
|
|
346
|
+
entry: 'Coming from context start',
|
|
347
|
+
type: 'text/plain',
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
stopContext: async () => {
|
|
352
|
+
state.isContextStarted = false;
|
|
353
|
+
},
|
|
354
|
+
onFailure: async (name) => {
|
|
355
|
+
return {
|
|
356
|
+
entry: `Failure in context ${String(context)} for test ${name}`,
|
|
357
|
+
type: 'text/plain',
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
state,
|
|
363
|
+
creator,
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
const setupFakeAsyncInteractor = ({ context, isContextStarted, }) => {
|
|
367
|
+
const state = {
|
|
368
|
+
isStarted: false,
|
|
369
|
+
isContextStarted: isContextStarted ?? false,
|
|
370
|
+
startArg: undefined,
|
|
371
|
+
};
|
|
372
|
+
const creator = async (arg) => ({
|
|
373
|
+
start: async () => {
|
|
374
|
+
state.isStarted = true;
|
|
375
|
+
state.startArg = arg;
|
|
376
|
+
},
|
|
377
|
+
stop: async () => {
|
|
378
|
+
state.isStarted = false;
|
|
379
|
+
},
|
|
380
|
+
startContext: async () => {
|
|
381
|
+
state.isContextStarted = true;
|
|
382
|
+
return {
|
|
383
|
+
context,
|
|
384
|
+
reportEntry$: of({
|
|
385
|
+
entry: 'Coming from context start',
|
|
386
|
+
type: 'text/plain',
|
|
387
|
+
}),
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
stopContext: async () => {
|
|
391
|
+
state.isContextStarted = false;
|
|
392
|
+
},
|
|
393
|
+
onFailure: async (name) => {
|
|
394
|
+
return {
|
|
395
|
+
entry: `Failure in context ${String(context)} for test ${name}`,
|
|
396
|
+
type: 'text/plain',
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
state,
|
|
402
|
+
creator,
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
const setupFakeRunner = () => {
|
|
406
|
+
const state = {
|
|
407
|
+
isStarted: false,
|
|
408
|
+
startArg: undefined,
|
|
409
|
+
};
|
|
410
|
+
const creator = (arg) => ({
|
|
411
|
+
start: async () => {
|
|
412
|
+
state.isStarted = true;
|
|
413
|
+
state.startArg = arg;
|
|
414
|
+
},
|
|
415
|
+
stop: async () => {
|
|
416
|
+
state.isStarted = false;
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
state,
|
|
421
|
+
creator,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
const setupFakeAsyncRunner = () => {
|
|
425
|
+
const state = {
|
|
426
|
+
isStarted: false,
|
|
427
|
+
startArg: undefined,
|
|
428
|
+
};
|
|
429
|
+
const creator = async (arg) => ({
|
|
430
|
+
start: async () => {
|
|
431
|
+
state.isStarted = true;
|
|
432
|
+
state.startArg = arg;
|
|
433
|
+
},
|
|
434
|
+
stop: async () => {
|
|
435
|
+
state.isStarted = false;
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
state,
|
|
440
|
+
creator,
|
|
441
|
+
};
|
|
442
|
+
};
|
package/type.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { EntryTuple, FilterRecord, GetKey, RecordToEntries } from '@dungarees/core/type-util.ts';
|
|
2
|
+
import type { Observable } from 'rxjs';
|
|
3
|
+
export type Runner = {
|
|
4
|
+
start: () => Promise<void>;
|
|
5
|
+
stop: () => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
export type Interactor<CONTEXT = unknown> = {
|
|
8
|
+
startContext: () => Promise<{
|
|
9
|
+
context: CONTEXT;
|
|
10
|
+
reportEntry$: Observable<ReportEntry>;
|
|
11
|
+
}>;
|
|
12
|
+
stopContext: () => Promise<void>;
|
|
13
|
+
onFailure: (testName: string) => Promise<ReportEntry>;
|
|
14
|
+
} & Runner;
|
|
15
|
+
export type InstanceEntry<NAME extends string = string, INSTANCE extends Interactor | Runner = Interactor | Runner> = EntryTuple<NAME, INSTANCE>;
|
|
16
|
+
export type ConfigEntry<NAME extends string = string, CONFIG extends ServiceConfig = ServiceConfig> = EntryTuple<NAME, CONFIG>;
|
|
17
|
+
export type ServiceConfig = InteractorConfig | RunnerConfig;
|
|
18
|
+
export type TestEnviornmentConfig = Record<string, ServiceConfig>;
|
|
19
|
+
export type GetInstance<CONFIG extends ServiceConfig> = Awaited<ReturnType<CONFIG['creator']>>;
|
|
20
|
+
export type GetInstanceEntry<CONFIG extends ConfigEntry> = CONFIG extends ConfigEntry<infer N, infer C> ? InstanceEntry<N, GetInstance<C>> : never;
|
|
21
|
+
export type GetContext<INTERACTOR extends Interactor | Runner> = INTERACTOR extends Interactor<infer CONTEXT> ? CONTEXT : never;
|
|
22
|
+
export type ReportEntry = {
|
|
23
|
+
entry: string;
|
|
24
|
+
type: 'text/plain';
|
|
25
|
+
} | {
|
|
26
|
+
entry: Buffer;
|
|
27
|
+
type: 'image/png';
|
|
28
|
+
};
|
|
29
|
+
export type DefaultConfig = {
|
|
30
|
+
hook?: 'before-all' | 'before';
|
|
31
|
+
};
|
|
32
|
+
export type RunnerConfig<RUNNER extends Runner = Runner, ARGS extends never[] = never[]> = {
|
|
33
|
+
type: 'runner';
|
|
34
|
+
creator: (...args: ARGS) => RUNNER | Promise<RUNNER>;
|
|
35
|
+
} & DefaultConfig;
|
|
36
|
+
export type InteractorConfig<INTERACTOR extends Interactor = Interactor, ARGS extends never[] = never[]> = {
|
|
37
|
+
type: 'interactor';
|
|
38
|
+
creator: (...args: ARGS) => INTERACTOR | Promise<INTERACTOR>;
|
|
39
|
+
} & DefaultConfig;
|
|
40
|
+
export type RunnerInstance = {
|
|
41
|
+
instance: Runner;
|
|
42
|
+
} & DefaultConfig;
|
|
43
|
+
export type InteractorInstance = {
|
|
44
|
+
instance: Interactor;
|
|
45
|
+
} & DefaultConfig;
|
|
46
|
+
export type TestEnviornmentState<SERVICES extends Record<string, ServiceConfig>, INTERACTORS extends InstanceEntry = GetInstanceEntry<RecordToEntries<FilterRecord<SERVICES, InteractorConfig>>>, RUNNERS extends InstanceEntry = GetInstanceEntry<RecordToEntries<FilterRecord<SERVICES, RunnerConfig>>>> = {
|
|
47
|
+
serviceConfigs: SERVICES;
|
|
48
|
+
interactors: Map<GetKey<INTERACTORS>, InteractorInstance>;
|
|
49
|
+
runners: Map<GetKey<RUNNERS>, RunnerInstance>;
|
|
50
|
+
};
|
package/type.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/world.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfigEntry, GetContext, GetInstance, InteractorConfig, ServiceConfig, TestEnviornmentState } from './type.ts';
|
|
2
|
+
import type { FilterRecord, GetKey, GetValueByKey, RecordToEntries } from '@dungarees/core/type-util.ts';
|
|
3
|
+
export type TestEnvironmentWorld<SERVICES extends Record<string, ServiceConfig>, INTERACTORS extends ConfigEntry = RecordToEntries<FilterRecord<SERVICES, InteractorConfig>>, SERVICE_NAMES extends GetKey<RecordToEntries<SERVICES>> = GetKey<RecordToEntries<SERVICES>>> = {
|
|
4
|
+
get: <NAME extends GetKey<INTERACTORS>>(name: NAME) => GetContext<GetInstance<GetValueByKey<INTERACTORS, NAME>>>;
|
|
5
|
+
register: (name: GetKey<INTERACTORS>, context: unknown) => void;
|
|
6
|
+
start: (name: SERVICE_NAMES, ...args: Parameters<SERVICES[SERVICE_NAMES]['creator']>) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare const createWorld: <SERVICES extends Record<string, ServiceConfig>>(state: TestEnviornmentState<SERVICES>) => TestEnvironmentWorld<SERVICES>;
|
package/world.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isInteractorName, isRunnerName } from './guards.js';
|
|
2
|
+
import { instantiateService } from './test-environment.js';
|
|
3
|
+
import { assertDefined, assertTypeByGuard } from '@dungarees/core/util.ts';
|
|
4
|
+
export const createWorld = (state) => {
|
|
5
|
+
const interactorContexts = new Map();
|
|
6
|
+
const get = (name) => assertTypeByGuard({
|
|
7
|
+
value: interactorContexts.get(name),
|
|
8
|
+
guard: (context) => context !== undefined,
|
|
9
|
+
message: `Interactor "${String(name)}" is not registered`,
|
|
10
|
+
});
|
|
11
|
+
const start = async (name, ...arg) => {
|
|
12
|
+
const service = assertDefined(state.serviceConfigs[name], `Service "${String(name)}" is not in the configuration`);
|
|
13
|
+
if (service.type === 'interactor' && isInteractorName(state.serviceConfigs, name)) {
|
|
14
|
+
const instantiatedService = await instantiateService(service, ...arg);
|
|
15
|
+
await instantiatedService.instance.start();
|
|
16
|
+
const { context } = await instantiatedService.instance.startContext();
|
|
17
|
+
register(name, context);
|
|
18
|
+
state.interactors.set(name, instantiatedService);
|
|
19
|
+
}
|
|
20
|
+
if (service.type === 'runner' && isRunnerName(state.serviceConfigs, name)) {
|
|
21
|
+
const instantiatedService = await instantiateService(service, ...arg);
|
|
22
|
+
await instantiatedService.instance.start();
|
|
23
|
+
state.runners.set(name, instantiatedService);
|
|
24
|
+
}
|
|
25
|
+
console.log(`${name}: Started in step`);
|
|
26
|
+
};
|
|
27
|
+
const register = (name, context) => {
|
|
28
|
+
interactorContexts.set(name, context);
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
get,
|
|
32
|
+
start,
|
|
33
|
+
register,
|
|
34
|
+
};
|
|
35
|
+
};
|