Skip to content

Commit 4959295

Browse files
authored
feat(test): jest 29 support (#4981)
add support for jest 29 in stencil. this commit is a fast-follow to #4979 (d3aa539), which adds support for jest 28. as such, there will be many similarlities between the two pieces of code. STENCIL-956
1 parent d3aa539 commit 4959295

29 files changed

Lines changed: 5535 additions & 4 deletions

.github/workflows/test-component-starter.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
jest: ['24', '25', '26', '27', '28']
19+
jest: ['24', '25', '26', '27', '28', '29']
2020
node: ['16', '18', '20']
2121
os: ['ubuntu-latest', 'windows-latest']
2222
runs-on: ${{ matrix.os }}

renovate.json5

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@
103103
matchPackageNames: ['@types/jest', 'jest'],
104104
allowedVersions: '<=28'
105105
},
106+
{
107+
"matchFileNames": ["src/testing/jest/jest-29/package.json"],
108+
matchPackageNames: ['@types/jest', 'jest'],
109+
allowedVersions: '<=29'
110+
},
106111
{
107112
// We intentionally run the karma tests against the oldest LTS of Node we support.
108113
// Prevent renovate from trying to bump node

src/sys/node/node-sys.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,9 +664,9 @@ export function createNodeSys(c: { process?: any; logger?: Logger } = {}): Compi
664664
const nodeResolve = new NodeResolveModule();
665665

666666
sys.lazyRequire = new NodeLazyRequire(nodeResolve, {
667-
'@types/jest': { minVersion: '24.9.1', recommendedVersion: '28', maxVersion: '28.0.0' },
668-
jest: { minVersion: '24.9.0', recommendedVersion: '28', maxVersion: '28.0.0' },
669-
'jest-cli': { minVersion: '24.9.0', recommendedVersion: '28', maxVersion: '28.0.0' },
667+
'@types/jest': { minVersion: '24.9.1', recommendedVersion: '29', maxVersion: '29.0.0' },
668+
jest: { minVersion: '24.9.0', recommendedVersion: '29', maxVersion: '29.0.0' },
669+
'jest-cli': { minVersion: '24.9.0', recommendedVersion: '29', maxVersion: '29.0.0' },
670670
puppeteer: { minVersion: '10.0.0', recommendedVersion: '20' },
671671
'puppeteer-core': { minVersion: '10.0.0', recommendedVersion: '20' },
672672
'workbox-build': { minVersion: '4.3.1', recommendedVersion: '4.3.1' },
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type { Config } from '@jest/types';
2+
import type * as d from '@stencil/core/internal';
3+
import { isString } from '@utils';
4+
5+
import { Jest29Stencil } from './jest-facade';
6+
7+
/**
8+
* Builds the `argv` to be used when programmatically invoking the Jest CLI
9+
* @param config the Stencil config to use while generating Jest CLI arguments
10+
* @returns the arguments to pass to the Jest CLI, wrapped in an object
11+
*/
12+
export function buildJestArgv(config: d.ValidatedConfig): Config.Argv {
13+
const yargs = require('yargs');
14+
15+
const knownArgs = config.flags.knownArgs.slice();
16+
17+
if (!knownArgs.some((a) => a.startsWith('--max-workers') || a.startsWith('--maxWorkers'))) {
18+
knownArgs.push(`--max-workers=${config.maxConcurrentWorkers}`);
19+
}
20+
21+
if (config.flags.devtools) {
22+
knownArgs.push('--runInBand');
23+
}
24+
25+
// we combine the modified args and the unknown args here and declare the
26+
// result read only, providing some type system-level assurance that we won't
27+
// mutate it after this point.
28+
//
29+
// We want that assurance because Jest likes to have any filepath match
30+
// patterns at the end of the args it receives. Those args are going to be
31+
// found in our `unknownArgs`, so while we want to do some stuff in this
32+
// function that adds to `knownArgs` we need a guarantee that all of the
33+
// `unknownArgs` are _after_ all the `knownArgs` in the array we end up
34+
// generating the Jest configuration from.
35+
const args: ReadonlyArray<string> = [...knownArgs, ...config.flags.unknownArgs];
36+
37+
config.logger.info(config.logger.magenta(`jest args: ${args.join(' ')}`));
38+
39+
const jestArgv = yargs(args).argv as Config.Argv;
40+
jestArgv.config = buildJestConfig(config);
41+
42+
if (typeof jestArgv.maxWorkers === 'string') {
43+
try {
44+
jestArgv.maxWorkers = parseInt(jestArgv.maxWorkers, 10);
45+
} catch (e) {}
46+
}
47+
48+
if (typeof jestArgv.ci === 'string') {
49+
jestArgv.ci = jestArgv.ci === 'true' || jestArgv.ci === '';
50+
}
51+
52+
return jestArgv;
53+
}
54+
55+
/**
56+
* Generate a Jest run configuration to be used as a part of the `argv` passed to the Jest CLI when it is invoked
57+
* programmatically
58+
* @param config the Stencil config to use while generating Jest CLI arguments
59+
* @returns the Jest Config to attach to the `argv` argument
60+
*/
61+
export function buildJestConfig(config: d.ValidatedConfig): string {
62+
const stencilConfigTesting = config.testing;
63+
const jestDefaults: Config.DefaultOptions = require('jest-config').defaults;
64+
65+
const validJestConfigKeys = Object.keys(jestDefaults);
66+
67+
const jestConfig: d.JestConfig = {};
68+
69+
Object.keys(stencilConfigTesting).forEach((key) => {
70+
if (validJestConfigKeys.includes(key)) {
71+
(jestConfig as any)[key] = (stencilConfigTesting as any)[key];
72+
}
73+
});
74+
75+
jestConfig.rootDir = config.rootDir;
76+
77+
if (isString(stencilConfigTesting.collectCoverage)) {
78+
jestConfig.collectCoverage = stencilConfigTesting.collectCoverage;
79+
}
80+
if (Array.isArray(stencilConfigTesting.collectCoverageFrom)) {
81+
jestConfig.collectCoverageFrom = stencilConfigTesting.collectCoverageFrom;
82+
}
83+
if (isString(stencilConfigTesting.coverageDirectory)) {
84+
jestConfig.coverageDirectory = stencilConfigTesting.coverageDirectory;
85+
}
86+
if (stencilConfigTesting.coverageThreshold) {
87+
jestConfig.coverageThreshold = stencilConfigTesting.coverageThreshold;
88+
}
89+
if (isString(stencilConfigTesting.globalSetup)) {
90+
jestConfig.globalSetup = stencilConfigTesting.globalSetup;
91+
}
92+
if (isString(stencilConfigTesting.globalTeardown)) {
93+
jestConfig.globalTeardown = stencilConfigTesting.globalTeardown;
94+
}
95+
if (isString(stencilConfigTesting.preset)) {
96+
jestConfig.preset = stencilConfigTesting.preset;
97+
}
98+
if (stencilConfigTesting.projects) {
99+
jestConfig.projects = stencilConfigTesting.projects;
100+
}
101+
if (Array.isArray(stencilConfigTesting.reporters)) {
102+
jestConfig.reporters = stencilConfigTesting.reporters;
103+
}
104+
if (isString(stencilConfigTesting.testResultsProcessor)) {
105+
jestConfig.testResultsProcessor = stencilConfigTesting.testResultsProcessor;
106+
}
107+
if (stencilConfigTesting.transform) {
108+
jestConfig.transform = stencilConfigTesting.transform;
109+
}
110+
if (stencilConfigTesting.verbose) {
111+
jestConfig.verbose = stencilConfigTesting.verbose;
112+
}
113+
114+
jestConfig.testRunner = new Jest29Stencil().getDefaultJestRunner();
115+
116+
return JSON.stringify(jestConfig);
117+
}
118+
119+
export function getProjectListFromCLIArgs(config: d.ValidatedConfig, argv: Config.Argv): string[] {
120+
const projects = argv.projects ? argv.projects : [];
121+
122+
projects.push(config.rootDir);
123+
124+
return projects;
125+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type { Circus } from '@jest/types';
2+
import type { E2EProcessEnv, JestEnvironmentGlobal } from '@stencil/core/internal';
3+
4+
import { connectBrowser, disconnectBrowser, newBrowserPage } from '../../puppeteer/puppeteer-browser';
5+
6+
export function createJestPuppeteerEnvironment() {
7+
const NodeEnvironment = require('jest-environment-node').TestEnvironment;
8+
const JestEnvironment = class extends NodeEnvironment {
9+
global: JestEnvironmentGlobal;
10+
browser: any = null;
11+
pages: any[] = [];
12+
testPath: string | null = null;
13+
14+
constructor(config: any, context: any) {
15+
super(config, context);
16+
this.testPath = context.testPath;
17+
}
18+
19+
async setup() {
20+
if ((process.env as E2EProcessEnv).__STENCIL_E2E_TESTS__ === 'true') {
21+
this.global.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
22+
this.global.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
23+
}
24+
}
25+
26+
/**
27+
* Jest Circus hook for capturing events.
28+
*
29+
* We use this lifecycle hook to capture information about the currently running test in the event that it is a
30+
* Jest-Stencil screenshot test, so that we may accurately report on it.
31+
*
32+
* @param event the captured runtime event
33+
*/
34+
async handleTestEvent(event: Circus.AsyncEvent): Promise<void> {
35+
// The 'parent' of a top-level describe block in a Jest block has one more 'parent', which is this string.
36+
// It is not exported by Jest, and is therefore copied here to exclude it from the fully qualified test name.
37+
const ROOT_DESCRIBE_BLOCK = 'ROOT_DESCRIBE_BLOCK';
38+
if (event.name === 'test_start') {
39+
const eventTest = event.test;
40+
41+
/**
42+
* We need to build the full name of the test for screenshot tests.
43+
* We do this as a test name can be the same across multiple tests - e.g. `it('renders', () => {...});`.
44+
* While this does not necessarily guarantee the generated name will be unique, it matches previous Jest-Stencil
45+
* screenshot behavior.
46+
*/
47+
let fullName = eventTest.name;
48+
let currentParent: Circus.DescribeBlock | undefined = eventTest.parent;
49+
// For each parent block (`describe('suite description', () => {...}`), grab the suite description and prepend
50+
// it to the running name.
51+
while (currentParent && currentParent.name && currentParent.name != ROOT_DESCRIBE_BLOCK) {
52+
fullName = `${currentParent.name} ${fullName}`;
53+
currentParent = currentParent.parent;
54+
}
55+
// Set the current spec for us to inspect for using the default reporter in screenshot tests.
56+
this.global.currentSpec = {
57+
// the event's test's name is analogous to the original description in earlier versions of jest
58+
description: eventTest.name,
59+
fullName,
60+
testPath: this.testPath,
61+
};
62+
}
63+
}
64+
async newPuppeteerPage() {
65+
if (!this.browser) {
66+
// load the browser and page on demand
67+
this.browser = await connectBrowser();
68+
}
69+
70+
const page = await newBrowserPage(this.browser);
71+
this.pages.push(page);
72+
// during E2E tests, we can safely assume that the current environment is a `E2EProcessEnv`
73+
const env: E2EProcessEnv = process.env as E2EProcessEnv;
74+
if (typeof env.__STENCIL_DEFAULT_TIMEOUT__ === 'string') {
75+
page.setDefaultTimeout(parseInt(env.__STENCIL_DEFAULT_TIMEOUT__, 10));
76+
}
77+
return page;
78+
}
79+
80+
async closeOpenPages() {
81+
await Promise.all(this.pages.map((page) => page.close()));
82+
this.pages.length = 0;
83+
}
84+
85+
async teardown() {
86+
await super.teardown();
87+
await this.closeOpenPages();
88+
await disconnectBrowser(this.browser);
89+
this.browser = null;
90+
}
91+
92+
getVmContext() {
93+
return super.getVmContext();
94+
}
95+
};
96+
97+
return JestEnvironment;
98+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// @ts-ignore - without importing this, we get a TypeScript error, "TS4053".
2+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3+
import type { Config } from '@jest/types';
4+
5+
import { JestFacade } from '../jest-facade';
6+
import { createJestPuppeteerEnvironment } from './jest-environment';
7+
import { jestPreprocessor } from './jest-preprocessor';
8+
import { preset } from './jest-preset';
9+
import { createTestRunner } from './jest-runner';
10+
import { runJest } from './jest-runner';
11+
import { runJestScreenshot } from './jest-screenshot';
12+
import { jestSetupTestFramework } from './jest-setup-test-framework';
13+
14+
/**
15+
* `JestFacade` implementation for communicating between this directory's version of Jest and Stencil
16+
*/
17+
export class Jest29Stencil implements JestFacade {
18+
getJestCliRunner() {
19+
return runJest;
20+
}
21+
22+
getRunJestScreenshot() {
23+
return runJestScreenshot;
24+
}
25+
26+
getDefaultJestRunner() {
27+
return 'jest-circus';
28+
}
29+
30+
getCreateJestPuppeteerEnvironment() {
31+
return createJestPuppeteerEnvironment;
32+
}
33+
34+
getJestPreprocessor() {
35+
return jestPreprocessor;
36+
}
37+
38+
getCreateJestTestRunner() {
39+
return createTestRunner;
40+
}
41+
42+
getJestSetupTestFramework() {
43+
return jestSetupTestFramework;
44+
}
45+
46+
getJestPreset() {
47+
return preset;
48+
}
49+
}

0 commit comments

Comments
 (0)