-
Notifications
You must be signed in to change notification settings - Fork 791
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add suport for jest 28 in stencil. this commit is the first pass at support a new version of jest in stencil in well over a year, and the first to use the new package-based architecture. this commit was generated using the steps that are outline in the README file of the jest directory, [available here](https://github.com/ionic-team/stencil/blob/d725ac9ef73f9700feb773d4a65e3ea988db6d31/src/testing/jest/README.md?plain=1). note: some commits of the original pr may be out of order with the readme above. although the readme is permalinked, it did undergo some changes between the time this pr's commits were created and the time this pr was created. some items of note: - the contents of `jest-27-and-under/` were copied into `jest-28/` using the latest available contents at the time - 6893954 (#4904). in order to support jest 28, the following changes were made: 1. projects are now typed as `string` in v28: the typings for jest are causing the project to fail compilation. update the return value here, as `Config.Path` is now just a string 1. resolve TS4053 error in v28 facade: we had to resolve a typescript compilation error where we would run into typescript error TS4053. this was caused by this facade's implementation of `getJestPreset` returning a Jest preset that referenced Jest v28's `Config` interface. the TypeScript compiler did not know how to make sense of this import within the module, requiring us to manually import it 1. update typings of the preprocessor: in jest 28, preprocessors cannot return a string. rather, they must at minimum return `{ code : string }`. this commit resolves those compilation errors by using Jest's `TransformedSource` type and updating the return values of the function accordingly 1. migrate to jest-circus for v28: switch users over to jest-circus for v28, rather than jest-jasmine2. this makes it such that we are not required to force users to add jest-jasmine2 to their dependencies, since it was removed from jest in v28 (and published as a separate package). 1. handle updated environment export: the test environments export has changed in v28. update the require statement to handle this change. 1. remove jasmine globals: remove direct references to jasmine globals that were used in jest 28 code. in some instnaces, puppeteer code that is not versioned (per version of jest) had to be modified to ensure that we did not attempt to reference a global `jasmine` that did not exist finally, some backwards compatability code was removed from the new `jest-28/` directory. it did not need to be executed in this new architecture (and can be siloed to `jest-27-and-under/`) closes: 3348 STENCIL-955
- Loading branch information
1 parent
d725ac9
commit d3aa539
Showing
34 changed files
with
5,525 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.