Skip to content

Add integration testing utilities#679

Open
crysadrak wants to merge 2 commits into
masterfrom
cnsqa-1710
Open

Add integration testing utilities#679
crysadrak wants to merge 2 commits into
masterfrom
cnsqa-1710

Conversation

@crysadrak

Copy link
Copy Markdown
Collaborator

Add integration testing utilities with initImaApp, clearImaApp, and setIntegrationConfig

What

  • Added a new ./integration export from @ima/testing-library with initImaApp, clearImaApp, setIntegrationConfig, getIntegrationConfig, aop, unAopAll, hookName, and createHook
  • Added ./jest-preset-integration/jest-preset export — a Jest preset for integration tests that omits the app/main module name mapper (the integration initImaApp loads the real app dynamically from a configurable appMainPath instead)
  • Extracted shared bootImaApp and validateJsdomEnvironment helpers into a new boot.ts module and re-used them in the existing RTL initImaApp
  • Added ambient type declaration for to-aop

Why

Integration tests need to boot the real IMA application (loaded dynamically from a configurable path) instead of a mocked app/main, run with a live router, wrap global timers for cleanup, and support per-suite boot config overrides — capabilities not covered by the existing unit-test initImaApp.

How it works

  • setIntegrationConfig / getIntegrationConfig hold configuration: appMainPath, rootDir, environment, prebootScript, TestPageRenderer, and boot config method overrides
  • initImaApp dynamically imports the app's main.js, wraps global timers (so they can be cleared after each test), calls $Router.listen() so IMA's route handler is active in jsdom, and merges boot config methods from both the default app functions and the per-suite overrides
  • clearImaApp restores native timers, clears the object container, and calls unAopAll
  • jest-preset-integration generates the jsdom HTML template the same way as jest-preset (via getIMAResponseContent) but without the app/main moduleNameMapper

@changeset-bot

changeset-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d836662

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@ima/testing-library Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@crysadrak crysadrak force-pushed the cnsqa-1710 branch 2 times, most recently from db41b2d to a6a34de Compare May 7, 2026 12:03
…nd `setIntegrationConfig`

Co-authored-by: Copilot <copilot@github.com>
- Changed the import path for testing integration from '@ima/plugin-testing-integration' to '@ima/testing-library/integration'.
- Enhanced the createIMAServer function to accept an explicit environmentName parameter.
- Added tests to ensure environment resolution occurs at the factory call rather than module load.
- Refactored environmentFactory to resolve environment names more cleanly and added support for explicit environment overrides.
- Updated TypeScript definitions to include environmentName in server configuration.
- Modified testing library to allow setting a specific environment for jsdom HTML template generation.
- Introduced new tests for booting the IMA app and integration app initialization to validate environment propagation and error handling.
@@ -0,0 +1,84 @@
import type { AppEnvironment } from '@ima/core';

@Filipoliko Filipoliko May 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot we unify the integration configuration under already existing client / server config? We could add integration key if we want to distinguish between unit and integration infrastructure. I am also seeing some overlaps here (appMainPath should be already available via import('app/main'), rootDir already exists in client config, environment already exists in server config).

onLoad = false,
}: BootImaAppOptions): Promise<ImaApp> {
if (environment !== undefined) {
setImaEnvironment(environment);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this do anything?

The environment is configured already when generating the JSDOM HTML template and should be already set to correct value. This override probably just sets the same value again, doesn't it?

Comment on lines +114 to +149
const appMainPath = path.isAbsolute(config.appMainPath)
? config.appMainPath
: path.resolve(config.rootDir, config.appMainPath);
const mainModule = await import(appMainPath);
const getInitialAppConfigFunctions =
mainModule.getInitialAppConfigFunctions ||
mainModule.default?.getInitialAppConfigFunctions;

if (!getInitialAppConfigFunctions) {
throw new Error(
`Cannot find getInitialAppConfigFunctions in ${config.appMainPath}. ` +
`Make sure the file exports getInitialAppConfigFunctions function.`
);
}

// Prefer the project's @ima/core export to ensure we use the same pluginLoader
// singleton. This is critical when the package is npm-linked, as the link would
// otherwise resolve to a separate @ima/core instance.
const ima = mainModule.ima || mainModule.default?.ima || imaFallback;

defaultBootConfigMethods =
typeof getInitialAppConfigFunctions === 'function'
? ((await getInitialAppConfigFunctions()) as BootConfigMethods)
: (getInitialAppConfigFunctions as BootConfigMethods);

app = await bootImaApp({
ima,
appConfigFunctions: {
initSettings: _mergeBootConfigMethod('initSettings'),
initBindApp: _mergeBootConfigMethod('initBindApp'),
initServicesApp: _mergeBootConfigMethod('initServicesApp'),
initRoutes: _mergeBootConfigMethod('initRoutes'),
},
environment: config.environment,
onLoad: true,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this could be simplified / unified a bit more with initImaApp from rtl.ts.

const mainModule = await import(appMainPath); could be replaced with top-level import { ima, getInitialAppConfigFunctions } from 'app/main';. And maybe we could also move ima definition inside of bootImaApp?

In the end, we could have just something like this in here.

import { getInitialAppConfigFunctions } from 'app/main';

// ...

if (!getInitialAppConfigFunctions) {
  throw new Error(
    `Cannot find getInitialAppConfigFunctions in ${config.appMainPath}. ` +
      `Make sure the file exports getInitialAppConfigFunctions function.`
  );
}

app = await bootImaApp({
  appConfigFunctions: {
    initSettings: _mergeBootConfigMethod('initSettings'),
    initBindApp: _mergeBootConfigMethod('initBindApp'),
    initServicesApp: _mergeBootConfigMethod('initServicesApp'),
    initRoutes: _mergeBootConfigMethod('initRoutes'),
  },
  onLoad: true,
});

Maybe it could be pushed even further and we could make _mergeBootConfigMethod a bit more generic and merge the default appConfigFunctions (from app/main) with the ones we send in bootImaApp inside bootImaApp. But we also need to merge configuration from args and integration config in this file and it could be a bit messy and confusing.

environment,
processEnvironment,
}) {
const env = resolveEnvironmentName(environment);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset file is missing for @ima/server.

@Filipoliko Filipoliko May 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually a bit worried about this change. It is fixing a bug, but it could be breaking for some use-cases. We are changing the moment of environment evaluation. This will fix @ima/storybook-integration to actually use regression environment when running storybook as opposed to value from NODE_ENV env variable.

Can we test this change in @ima/server to see if it affects our services especially in storybook visual regression tests?

});

// To use IMA route handler in jsdom
(app.oc.get('$Router') as any).listen();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be typed as ClientRouter from @ima/core

*/
export async function initImaApp(
bootConfigMethods: BootConfigMethods = {}
): Promise<ImaApp & Record<string, unknown>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should introduce an internal type ImaAppExtended = ImaApp & Record<string, unknown>, since we repeat this construction in multiple places.

const config = getIntegrationConfig();
const { TestPageRenderer } = config;
const pageRendererResults: unknown[] = [];
let app: (ImaApp & Record<string, unknown>) | null = null;

@Filipoliko Filipoliko May 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be typed only as ImaApp | null?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there even a reason for this to be pre-defined here and filled with value later? Cannot we just define the variable later?

validateJsdomEnvironment();

const config = getIntegrationConfig();
const { TestPageRenderer } = config;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should remove support for TestPageRenderer configuration? The original idea was to hypothetically implement custom renderers as an alternative to @ima/react-page-renderer. For example using Enzyme, or React Testing Library. But we were never able to really use it anywhere. Since we removed the only available test page renderer (Enzyme) with this migration, maybe we should drop support for this functionality completely?

If somebody still needs this, users can actually extend initBindApp boot config method to override the default page renderer. We don't really need this custom config to accomplish this. It waas supposed to be just a convinience API, but it never made it to a real world scenario.

* This simulates browser behavior where the URL is already set in the address bar.
*/
export function initBindApp(ns: Namespace, oc: ObjectContainer): void {
const Router = (oc.get('$Router') as any).constructor;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type here could also be ClientRouter

hookName.beforeMethod,
'route',
({ args, context }: { args: any[]; context: any }) => {
const pageManager = oc.get('$PageManager') as any;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a real type could be used here

const pageRendererResults: unknown[] = [];
let app: (ImaApp & Record<string, unknown>) | null = null;
let result: (ImaApp & Record<string, unknown>) | null = null;
let defaultBootConfigMethods: BootConfigMethods = {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as app variable, do we really need to pre-define the variable here on top? Cannot we just declare it when its being filled with a value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants