Skip to content

Commit

Permalink
wip2
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph committed Nov 25, 2023
1 parent 8c659c4 commit e9e267b
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 136 deletions.
72 changes: 44 additions & 28 deletions docs/jest-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,52 @@ If you already have a custom test environment, you can extend it via composition

## Manual integration

If your use case is not covered by the above, you can call `jest-metadata` lifecycle methods manually:
If your use case is not covered by the above, you can rewrite your test environment
into a listener of `jest-environment-emit` events. Here is an example of how to do it:

```js title="jest.config.js"
/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'jest-environment-emit',
testEnvironmentOptions: {
listeners: [
'jest-metadata/environment-listener',
['your-project/listener', { /* options */ }],
],
},
};
```

```diff
+ import * as jmHooks from 'jest-metadata/environment-hooks';

class MyCustomEnvironment extends NodeEnvironment {
constructor(config, context) {
super(config, context);
+ jmHooks.onTestEnvironmentCreate(this, config, context);
}

async setup() {
await super.setup();
+ await jmHooks.onTestEnvironmentSetup(this);
}

async teardown() {
+ await jmHooks.onTestEnvironmentTeardown(this);
await super.teardown();
}

async handleTestEvent(event, state) {
+ await jmHooks.onTestEnvironmentHandleTestEvent(this, event, state);
// ...
}
where `your-project/listener` file contains the following code:

```js title="your-project/listeners.js"
import * as jee from 'jest-environment-emit';

const listener: jee.EnvironmentListenerFn = (context, yourOptions) => {
context.testEvents
.on('test_environment_setup', ({ env }: jee.TestEnvironmentSetupEvent) => {
// ...
})
.on('test_start', ({ event, state }: jee.TestEnvironmentCircusEvent) => {
// ...
})
.on('test_done', ({ event, state }: jee.TestEnvironmentCircusEvent) => {
// ...
})
.on('test_environment_teardown', ({ env }: jee.TestEnvironmentTeardownEvent) => {
// ...
});
};

export default listener;
```

## Lifecycle of `jest-metadata` test environment

Here is a brief description of each lifecycle method:

* `onTestEnvironmentCreate` - called when the test environment is created. This is the first lifecycle method to be called. It injects `__JEST_METADATA__` context into `this.global` object to eliminate sandboxing issues.
* `onTestEnvironmentSetup` - called when the test environment is set up. This is the second lifecycle method to be called. It initializes `jest-metadata` IPC client if Jest is running in a multi-worker mode to enable communication with the main process where the reporters reside.
* `onTestEnvironmentHandleTestEvent` - called when the test environment receives a test event from `jest-circus`. This method is called many times during the test run. It is responsible for building a metadata tree, parallel to `jest-circus` State tree, and for sending test events to the main process.
* `onTestEnvironmentTeardown` - called when the test environment is torn down. This is the last lifecycle method to be called. It is responsible for shutting down the IPC client and for sending the final test event to the main process.
* `test_environment_setup`:
* injects `__JEST_METADATA__` context into `this.global` object to eliminate sandboxing issues;
* initializes `jest-metadata` IPC client if Jest is running in a multi-worker mode to enable communication with the main process where the reporters reside;
* all `jest-circus` events coming to `handleTestEvent` – building a metadata tree, parallel to `jest-circus` State tree, and for sending test events to the main process.
* `test_environment_teardown` – responsible for shutting down the IPC client and for sending the final test event to the main process.
2 changes: 1 addition & 1 deletion environment-decorator.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* Jest 27 fallback */
module.exports = require('./dist/environment-decorator');
module.exports = require('./dist/environment-listener');
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"require": "./dist/environment-decorator.js",
"types": "./dist/environment-decorator.d.ts"
},
"./environment-listener": {
"import": "./dist/environment-listener.js",
"require": "./dist/environment-listener.js",
"types": "./dist/environment-listener.d.ts"
},
"./environment-jsdom": {
"import": "./dist/environment-jsdom.js",
"require": "./dist/environment-jsdom.js",
Expand Down Expand Up @@ -86,7 +91,7 @@
"bunyan": "^2.0.5",
"bunyan-debug-stream": "^3.1.0",
"funpermaproxy": "^1.1.0",
"jest-environment-emit": "^1.0.0-alpha.2",
"jest-environment-emit": "^1.0.0-alpha.4",
"lodash.merge": "^4.6.2",
"node-ipc": "9.2.1",
"strip-ansi": "^6.0.0",
Expand Down
111 changes: 5 additions & 106 deletions src/environment-decorator.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,7 @@
import { inspect } from 'util';
import type { EmitterSubscriptionContext, TestEnvironmentCircusEvent } from 'jest-environment-emit';
import type { JestEnvironment } from '@jest/environment';
import WithEmitter from 'jest-environment-emit';
import { JestMetadataError } from './errors';
import { detectDuplicateRealms, injectRealmIntoSandbox, realm } from './realms';
import { jestUtils, logger } from './utils';
import listener from './environment-listener';

const log = logger.child({ cat: 'environment', tid: 'environment' });

WithEmitter.subscribe((context: EmitterSubscriptionContext) => {
const jestEnvironment = context.env;
const jestEnvironmentConfig = context.config;
const environmentContext = context.context;

detectDuplicateRealms(true);
injectRealmIntoSandbox(jestEnvironment.global, realm);
const testFilePath = environmentContext.testPath;
realm.environmentHandler.handleEnvironmentCreated(testFilePath);
realm.events.add(realm.setEmitter);

if (!realm.globalMetadata.hasTestFileMetadata(testFilePath)) {
realm.coreEmitter.emit({
type: 'add_test_file',
testFilePath,
});

if (realm.type === 'parent_process') {
const { globalConfig } = jestEnvironmentConfig;
const first = <T>(r: T[]) => r[0];
const hint = globalConfig
? ` "reporters": ${inspect(globalConfig.reporters?.map(first))}\n`
: ''; // Jest 27 fallback

const message =
`Cannot use a metadata test environment without a metadata server.\n` +
`Please check that at least one of the reporters in your Jest config inherits from "jest-metadata/reporter".\n` +
hint;

if (
globalConfig &&
(jestUtils.isSingleWorker(globalConfig) || jestUtils.isInsideIDE(globalConfig))
) {
log.warn(message);
} else {
log.debug(message);
throw new JestMetadataError(message);
}
}
}

const testEventHandler = ({ event, state }: TestEnvironmentCircusEvent) => {
realm.environmentHandler.handleTestEvent(event, state);
};

const flushHandler = () => realm.ipc.flush();

context.testEvents
.on(
'test_environment_setup',
async function () {
if (realm.type === 'child_process') {
await realm.ipc.start();
}
},
-1,
)
.on(
'test_environment_teardown',
async function () {
detectDuplicateRealms(false);

if (realm.type === 'child_process') {
await realm.ipc.stop();
}
},
Number.MAX_SAFE_INTEGER,
)
.on('setup', testEventHandler, -1)
.on('include_test_location_in_result', testEventHandler, -1)
.on('start_describe_definition', testEventHandler, -1)
.on('finish_describe_definition', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('add_hook', testEventHandler, -1)
.on('add_test', testEventHandler, -1)
.on('run_start', testEventHandler, -1)
.on('run_start', flushHandler, Number.MAX_SAFE_INTEGER)
.on('run_describe_start', testEventHandler, -1)
.on('hook_failure', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('hook_start', testEventHandler, -1)
.on('hook_success', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_start', testEventHandler, -1)
.on('test_start', flushHandler, Number.MAX_SAFE_INTEGER)
.on('test_started', testEventHandler, -1)
.on('test_retry', testEventHandler, -1)
.on('test_skip', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_todo', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_fn_start', testEventHandler, -1)
.on('test_fn_failure', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_fn_success', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_done', testEventHandler, Number.MAX_SAFE_INTEGER - 1)
.on('test_done', flushHandler, Number.MAX_SAFE_INTEGER)
.on('run_describe_finish', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('run_finish', testEventHandler, Number.MAX_SAFE_INTEGER - 1)
.on('run_finish', flushHandler, Number.MAX_SAFE_INTEGER)
.on('teardown', testEventHandler, Number.MAX_SAFE_INTEGER);
});

export const WithMetadata = WithEmitter;
export default WithMetadata;
export const WithMetadata = <E extends JestEnvironment = JestEnvironment>(
JestEnvironmentClass: new (...args: any[]) => E,
) => WithEmitter<E>(JestEnvironmentClass, listener, 'WithMetadata');
106 changes: 106 additions & 0 deletions src/environment-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { inspect } from 'util';
import type { EnvironmentListenerFn, TestEnvironmentCircusEvent } from 'jest-environment-emit';
import { JestMetadataError } from './errors';
import { detectDuplicateRealms, injectRealmIntoSandbox, realm } from './realms';
import { jestUtils, logger } from './utils';

const log = logger.child({ cat: 'environment', tid: 'environment' });

const listener: EnvironmentListenerFn = (context) => {
const jestEnvironment = context.env;
const jestEnvironmentConfig = context.config;
const environmentContext = context.context;

detectDuplicateRealms(true);
injectRealmIntoSandbox(jestEnvironment.global, realm);
const testFilePath = environmentContext.testPath;
realm.environmentHandler.handleEnvironmentCreated(testFilePath);
realm.events.add(realm.setEmitter);

if (!realm.globalMetadata.hasTestFileMetadata(testFilePath)) {
realm.coreEmitter.emit({
type: 'add_test_file',
testFilePath,
});

if (realm.type === 'parent_process') {
const { globalConfig } = jestEnvironmentConfig;
const first = <T>(r: T[]) => r[0];
const hint = globalConfig
? ` "reporters": ${inspect(globalConfig.reporters?.map(first))}\n`
: ''; // Jest 27 fallback

const message =
`Cannot use a metadata test environment without a metadata server.\n` +
`Please check that at least one of the reporters in your Jest config inherits from "jest-metadata/reporter".\n` +
hint;

if (
globalConfig &&
(jestUtils.isSingleWorker(globalConfig) || jestUtils.isInsideIDE(globalConfig))
) {
log.warn(message);
} else {
log.debug(message);
throw new JestMetadataError(message);
}
}
}

const testEventHandler = ({ event, state }: TestEnvironmentCircusEvent) => {
realm.environmentHandler.handleTestEvent(event, state);
};

const flushHandler = () => realm.ipc.flush();

context.testEvents
.on(
'test_environment_setup',
async function () {
if (realm.type === 'child_process') {
await realm.ipc.start();
}
},
-1,
)
.on(
'test_environment_teardown',
async function () {
detectDuplicateRealms(false);

if (realm.type === 'child_process') {
await realm.ipc.stop();
}
},
Number.MAX_SAFE_INTEGER,
)
.on('setup', testEventHandler, -1)
.on('include_test_location_in_result', testEventHandler, -1)
.on('start_describe_definition', testEventHandler, -1)
.on('finish_describe_definition', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('add_hook', testEventHandler, -1)
.on('add_test', testEventHandler, -1)
.on('run_start', testEventHandler, -1)
.on('run_start', flushHandler, Number.MAX_SAFE_INTEGER)
.on('run_describe_start', testEventHandler, -1)
.on('hook_failure', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('hook_start', testEventHandler, -1)
.on('hook_success', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_start', testEventHandler, -1)
.on('test_start', flushHandler, Number.MAX_SAFE_INTEGER)
.on('test_started', testEventHandler, -1)
.on('test_retry', testEventHandler, -1)
.on('test_skip', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_todo', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_fn_start', testEventHandler, -1)
.on('test_fn_failure', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_fn_success', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('test_done', testEventHandler, Number.MAX_SAFE_INTEGER - 1)
.on('test_done', flushHandler, Number.MAX_SAFE_INTEGER)
.on('run_describe_finish', testEventHandler, Number.MAX_SAFE_INTEGER)
.on('run_finish', testEventHandler, Number.MAX_SAFE_INTEGER - 1)
.on('run_finish', flushHandler, Number.MAX_SAFE_INTEGER)
.on('teardown', testEventHandler, Number.MAX_SAFE_INTEGER);
};

export default listener;

0 comments on commit e9e267b

Please sign in to comment.