Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(test): jest 28 support #4979

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-component-starter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
jest: ['24', '25', '26', '27']
jest: ['24', '25', '26', '27', '28']
node: ['16', '18', '20']
os: ['ubuntu-latest', 'windows-latest']
runs-on: ${{ matrix.os }}
Expand Down
5 changes: 5 additions & 0 deletions renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
matchPackageNames: ['@types/jest'],
allowedVersions: '<=27'
},
{
"matchFileNames": ["src/testing/jest/jest-28/package.json"],
matchPackageNames: ['@types/jest', 'jest'],
allowedVersions: '<=28'
},
{
// We intentionally run the karma tests against the oldest LTS of Node we support.
// Prevent renovate from trying to bump node
Expand Down
4 changes: 2 additions & 2 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2097,10 +2097,10 @@ export interface JestEnvironmentGlobal {
h: any;
resourcesUrl: string;
currentSpec?: {
id: string;
id?: string;
description: string;
fullName: string;
testPath: string;
testPath: string | null;
};
env: { [prop: string]: string };
screenshotDescriptions: Set<string>;
Expand Down
12 changes: 11 additions & 1 deletion src/screenshot/screenshot-compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,17 @@ export async function compareScreenshot(

async function getMismatchedPixels(pixelmatchModulePath: string, pixelMatchInput: d.PixelMatchInput) {
return new Promise<number>((resolve, reject) => {
const timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL * 0.5;
/**
* When using screenshot functionality in a runner that is not Jasmine (e.g. Jest Circus), we need to set a default
* value for timeouts. There are runtime errors that occur if we attempt to use optional chaining + nullish
* coalescing with the `jasmine` global stating it's not defined. As a result, we use a ternary here.
*
* The '2500' value that we default to is the value of `jasmine.DEFAULT_TIMEOUT_INTERVAL` (5000) divided by 2.
*/
const timeout =
typeof jasmine !== 'undefined' && jasmine.DEFAULT_TIMEOUT_INTERVAL
? jasmine.DEFAULT_TIMEOUT_INTERVAL * 0.5
: 2500;
const tmr = setTimeout(() => {
reject(`getMismatchedPixels timeout: ${timeout}ms`);
}, timeout);
Expand Down
6 changes: 3 additions & 3 deletions src/sys/node/node-sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,9 +664,9 @@ export function createNodeSys(c: { process?: any; logger?: Logger } = {}): Compi
const nodeResolve = new NodeResolveModule();

sys.lazyRequire = new NodeLazyRequire(nodeResolve, {
'@types/jest': { minVersion: '24.9.1', recommendedVersion: '27.0.3', maxVersion: '27.0.0' },
jest: { minVersion: '24.9.0', recommendedVersion: '27.0.3', maxVersion: '27.0.0' },
'jest-cli': { minVersion: '24.9.0', recommendedVersion: '27.4.5', maxVersion: '27.0.0' },
'@types/jest': { minVersion: '24.9.1', recommendedVersion: '28', maxVersion: '28.0.0' },
jest: { minVersion: '24.9.0', recommendedVersion: '28', maxVersion: '28.0.0' },
'jest-cli': { minVersion: '24.9.0', recommendedVersion: '28', maxVersion: '28.0.0' },
puppeteer: { minVersion: '10.0.0', recommendedVersion: '20' },
'puppeteer-core': { minVersion: '10.0.0', recommendedVersion: '20' },
'workbox-build': { minVersion: '4.3.1', recommendedVersion: '4.3.1' },
Expand Down
125 changes: 125 additions & 0 deletions src/testing/jest/jest-28/jest-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Config } from '@jest/types';
import type * as d from '@stencil/core/internal';
import { isString } from '@utils';

import { Jest28Stencil } from './jest-facade';

/**
* Builds the `argv` to be used when programmatically invoking the Jest CLI
* @param config the Stencil config to use while generating Jest CLI arguments
* @returns the arguments to pass to the Jest CLI, wrapped in an object
*/
export function buildJestArgv(config: d.ValidatedConfig): Config.Argv {
const yargs = require('yargs');

const knownArgs = config.flags.knownArgs.slice();

if (!knownArgs.some((a) => a.startsWith('--max-workers') || a.startsWith('--maxWorkers'))) {
knownArgs.push(`--max-workers=${config.maxConcurrentWorkers}`);
}

if (config.flags.devtools) {
knownArgs.push('--runInBand');
}

// we combine the modified args and the unknown args here and declare the
// result read only, providing some type system-level assurance that we won't
// mutate it after this point.
//
// We want that assurance because Jest likes to have any filepath match
// patterns at the end of the args it receives. Those args are going to be
// found in our `unknownArgs`, so while we want to do some stuff in this
// function that adds to `knownArgs` we need a guarantee that all of the
// `unknownArgs` are _after_ all the `knownArgs` in the array we end up
// generating the Jest configuration from.
const args: ReadonlyArray<string> = [...knownArgs, ...config.flags.unknownArgs];

config.logger.info(config.logger.magenta(`jest args: ${args.join(' ')}`));

const jestArgv = yargs(args).argv as Config.Argv;
jestArgv.config = buildJestConfig(config);

if (typeof jestArgv.maxWorkers === 'string') {
try {
jestArgv.maxWorkers = parseInt(jestArgv.maxWorkers, 10);
} catch (e) {}
}

if (typeof jestArgv.ci === 'string') {
jestArgv.ci = jestArgv.ci === 'true' || jestArgv.ci === '';
}

return jestArgv;
}

/**
* Generate a Jest run configuration to be used as a part of the `argv` passed to the Jest CLI when it is invoked
* programmatically
* @param config the Stencil config to use while generating Jest CLI arguments
* @returns the Jest Config to attach to the `argv` argument
*/
export function buildJestConfig(config: d.ValidatedConfig): string {
const stencilConfigTesting = config.testing;
const jestDefaults: Config.DefaultOptions = require('jest-config').defaults;

const validJestConfigKeys = Object.keys(jestDefaults);

const jestConfig: d.JestConfig = {};

Object.keys(stencilConfigTesting).forEach((key) => {
if (validJestConfigKeys.includes(key)) {
(jestConfig as any)[key] = (stencilConfigTesting as any)[key];
}
});

jestConfig.rootDir = config.rootDir;

if (isString(stencilConfigTesting.collectCoverage)) {
jestConfig.collectCoverage = stencilConfigTesting.collectCoverage;
}
if (Array.isArray(stencilConfigTesting.collectCoverageFrom)) {
jestConfig.collectCoverageFrom = stencilConfigTesting.collectCoverageFrom;
}
if (isString(stencilConfigTesting.coverageDirectory)) {
jestConfig.coverageDirectory = stencilConfigTesting.coverageDirectory;
}
if (stencilConfigTesting.coverageThreshold) {
jestConfig.coverageThreshold = stencilConfigTesting.coverageThreshold;
}
if (isString(stencilConfigTesting.globalSetup)) {
jestConfig.globalSetup = stencilConfigTesting.globalSetup;
}
if (isString(stencilConfigTesting.globalTeardown)) {
jestConfig.globalTeardown = stencilConfigTesting.globalTeardown;
}
if (isString(stencilConfigTesting.preset)) {
jestConfig.preset = stencilConfigTesting.preset;
}
if (stencilConfigTesting.projects) {
jestConfig.projects = stencilConfigTesting.projects;
}
if (Array.isArray(stencilConfigTesting.reporters)) {
jestConfig.reporters = stencilConfigTesting.reporters;
}
if (isString(stencilConfigTesting.testResultsProcessor)) {
jestConfig.testResultsProcessor = stencilConfigTesting.testResultsProcessor;
}
if (stencilConfigTesting.transform) {
jestConfig.transform = stencilConfigTesting.transform;
}
if (stencilConfigTesting.verbose) {
jestConfig.verbose = stencilConfigTesting.verbose;
}

jestConfig.testRunner = new Jest28Stencil().getDefaultJestRunner();

return JSON.stringify(jestConfig);
}

export function getProjectListFromCLIArgs(config: d.ValidatedConfig, argv: Config.Argv): string[] {
const projects = argv.projects ? argv.projects : [];

projects.push(config.rootDir);

return projects;
}
98 changes: 98 additions & 0 deletions src/testing/jest/jest-28/jest-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Circus } from '@jest/types';
import type { E2EProcessEnv, JestEnvironmentGlobal } from '@stencil/core/internal';

import { connectBrowser, disconnectBrowser, newBrowserPage } from '../../puppeteer/puppeteer-browser';

export function createJestPuppeteerEnvironment() {
const NodeEnvironment = require('jest-environment-node').TestEnvironment;
const JestEnvironment = class extends NodeEnvironment {
global: JestEnvironmentGlobal;
browser: any = null;
pages: any[] = [];
testPath: string | null = null;

constructor(config: any, context: any) {
super(config, context);
this.testPath = context.testPath;
}

async setup() {
if ((process.env as E2EProcessEnv).__STENCIL_E2E_TESTS__ === 'true') {
this.global.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
this.global.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
}
}

/**
* Jest Circus hook for capturing events.
*
* We use this lifecycle hook to capture information about the currently running test in the event that it is a
* Jest-Stencil screenshot test, so that we may accurately report on it.
*
* @param event the captured runtime event
*/
async handleTestEvent(event: Circus.AsyncEvent): Promise<void> {
// The 'parent' of a top-level describe block in a Jest block has one more 'parent', which is this string.
// It is not exported by Jest, and is therefore copied here to exclude it from the fully qualified test name.
const ROOT_DESCRIBE_BLOCK = 'ROOT_DESCRIBE_BLOCK';
if (event.name === 'test_start') {
const eventTest = event.test;

/**
* We need to build the full name of the test for screenshot tests.
* We do this as a test name can be the same across multiple tests - e.g. `it('renders', () => {...});`.
* While this does not necessarily guarantee the generated name will be unique, it matches previous Jest-Stencil
* screenshot behavior.
*/
let fullName = eventTest.name;
let currentParent: Circus.DescribeBlock | undefined = eventTest.parent;
// For each parent block (`describe('suite description', () => {...}`), grab the suite description and prepend
// it to the running name.
while (currentParent && currentParent.name && currentParent.name != ROOT_DESCRIBE_BLOCK) {
fullName = `${currentParent.name} ${fullName}`;
currentParent = currentParent.parent;
}
// Set the current spec for us to inspect for using the default reporter in screenshot tests.
this.global.currentSpec = {
// the event's test's name is analogous to the original description in earlier versions of jest
description: eventTest.name,
fullName,
testPath: this.testPath,
};
}
}
async newPuppeteerPage() {
if (!this.browser) {
// load the browser and page on demand
this.browser = await connectBrowser();
}

const page = await newBrowserPage(this.browser);
this.pages.push(page);
// during E2E tests, we can safely assume that the current environment is a `E2EProcessEnv`
const env: E2EProcessEnv = process.env as E2EProcessEnv;
if (typeof env.__STENCIL_DEFAULT_TIMEOUT__ === 'string') {
page.setDefaultTimeout(parseInt(env.__STENCIL_DEFAULT_TIMEOUT__, 10));
}
return page;
}

async closeOpenPages() {
await Promise.all(this.pages.map((page) => page.close()));
this.pages.length = 0;
}

async teardown() {
await super.teardown();
await this.closeOpenPages();
await disconnectBrowser(this.browser);
this.browser = null;
}

getVmContext() {
return super.getVmContext();
}
};

return JestEnvironment;
}
49 changes: 49 additions & 0 deletions src/testing/jest/jest-28/jest-facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @ts-ignore - without importing this, we get a TypeScript error, "TS4053".
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Config } from '@jest/types';

import { JestFacade } from '../jest-facade';
import { createJestPuppeteerEnvironment } from './jest-environment';
import { jestPreprocessor } from './jest-preprocessor';
import { preset } from './jest-preset';
import { createTestRunner } from './jest-runner';
import { runJest } from './jest-runner';
import { runJestScreenshot } from './jest-screenshot';
import { jestSetupTestFramework } from './jest-setup-test-framework';

/**
* `JestFacade` implementation for communicating between this directory's version of Jest and Stencil
*/
export class Jest28Stencil implements JestFacade {
getJestCliRunner() {
return runJest;
}

getRunJestScreenshot() {
return runJestScreenshot;
}

getDefaultJestRunner() {
return 'jest-circus';
}

getCreateJestPuppeteerEnvironment() {
return createJestPuppeteerEnvironment;
}

getJestPreprocessor() {
return jestPreprocessor;
}

getCreateJestTestRunner() {
return createTestRunner;
}

getJestSetupTestFramework() {
return jestSetupTestFramework;
}

getJestPreset() {
return preset;
}
}
Loading
Loading