From 5bfe4ff2e309ec27a779a8c096ea6b87998c1b06 Mon Sep 17 00:00:00 2001 From: John Crim Date: Thu, 24 Feb 2022 09:41:26 -0800 Subject: [PATCH 1/6] feat(jest esm): Initial support for running Jest using ESM This commit does not have test runner support, only works for running `cd test/jest-esm; npm run jest`. --- .gitignore | 1 + src/declarations/stencil-public-compiler.ts | 19 +++++++ src/testing/jest/jest-config.ts | 7 ++- src/testing/jest/jest-preprocessor.ts | 37 +++++++++---- src/testing/jest/jest-setup-test-framework.ts | 2 + .../jest/test/jest-preprocessor.spec.ts | 54 ++++++++++++++----- src/testing/test-transpile.ts | 1 - test/jest-esm/jest.config.cjs | 38 +++++++++++++ test/jest-esm/package.json | 10 ++++ .../src/components/mixed/mixed.spec.ts | 25 +++++++++ test/jest-esm/src/components/mixed/mixed.tsx | 21 ++++++++ .../src/components/simple/simple.spec.ts | 24 +++++++++ .../jest-esm/src/components/simple/simple.tsx | 11 ++++ test/jest-esm/src/components/utils/as-cjs.cjs | 8 +++ .../src/components/utils/as-js-esm.js | 4 ++ test/jest-esm/src/components/utils/as-mjs.mjs | 5 ++ test/jest-esm/src/components/utils/as-ts.ts | 4 ++ .../src/components/utils/deep-cjs.cjs | 3 ++ .../src/components/utils/deep-js-esm.js | 3 ++ .../src/components/utils/deep-mjs.mjs | 3 ++ test/jest-esm/src/components/utils/deep-ts.ts | 3 ++ test/jest-esm/stencil.config.ts | 7 +++ test/jest-esm/tsconfig.json | 26 +++++++++ 23 files changed, 286 insertions(+), 30 deletions(-) create mode 100644 test/jest-esm/jest.config.cjs create mode 100644 test/jest-esm/package.json create mode 100644 test/jest-esm/src/components/mixed/mixed.spec.ts create mode 100644 test/jest-esm/src/components/mixed/mixed.tsx create mode 100644 test/jest-esm/src/components/simple/simple.spec.ts create mode 100644 test/jest-esm/src/components/simple/simple.tsx create mode 100644 test/jest-esm/src/components/utils/as-cjs.cjs create mode 100644 test/jest-esm/src/components/utils/as-js-esm.js create mode 100644 test/jest-esm/src/components/utils/as-mjs.mjs create mode 100644 test/jest-esm/src/components/utils/as-ts.ts create mode 100644 test/jest-esm/src/components/utils/deep-cjs.cjs create mode 100644 test/jest-esm/src/components/utils/deep-js-esm.js create mode 100644 test/jest-esm/src/components/utils/deep-mjs.mjs create mode 100644 test/jest-esm/src/components/utils/deep-ts.ts create mode 100644 test/jest-esm/stencil.config.ts create mode 100644 test/jest-esm/tsconfig.json diff --git a/.gitignore b/.gitignore index 7c50504c14d..9f70ae427ba 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ dist/ test/**/www/* test/**/hydrate/* +test/**/components.d.ts .stencil /dependencies.json coverage/** diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 419ba55c716..79e5de8817c 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -1552,6 +1552,17 @@ export interface JestConfig { coverageThreshold?: any; errorOnDeprecated?: boolean; + + /** + * Jest will run .mjs and .js files with nearest package.json's type field set to `module` as ECMAScript Modules. If you have + * any other files that should run with native ESM, you need to specify their file extension here. + * + * Default is `['.ts', '.tsx', '.jsx']`. + * + * Note: Jest's ESM support is still experimental, see [its docs for more details]{@link https://jestjs.io/docs/ecmascript-modules}. + */ + extensionsToTreatAsEsm?: string[]; + forceCoverageMatch?: any[]; globals?: any; globalSetup?: string; @@ -1697,6 +1708,14 @@ export interface TestingConfig extends JestConfig { */ screenshotConnector?: string; + /** + * If `true`, Jest is run with ES Modules enabled. Requires node be run with `--experimental-vm-modules` enabled. + * If `false` or not set, Jest is run in CommonJS mode, which uses CommonJS imports - this is the standard behavior + * for Jest. In CommonJS mode, all ESM files must be transpiled to CommonJS. + * @see https://jestjs.io/docs/ecmascript-modules + */ + useESModules?: boolean; + /** * Amount of time in milliseconds to wait before a screenshot is taken. */ diff --git a/src/testing/jest/jest-config.ts b/src/testing/jest/jest-config.ts index 5feda9fedb2..61481f21346 100644 --- a/src/testing/jest/jest-config.ts +++ b/src/testing/jest/jest-config.ts @@ -48,13 +48,12 @@ export function buildJestArgv(config: d.Config): Config.Argv { const args = [...config.flags.unknownArgs.slice(), ...config.flags.knownArgs.slice()]; - if (!args.some((a) => a.startsWith('--max-workers') || a.startsWith('--maxWorkers'))) { - args.push(`--max-workers=${config.maxConcurrentWorkers}`); - } - if (config.flags.devtools) { args.push('--runInBand'); } + else if (!args.some((a) => a.startsWith('--max-workers') || (a === '-i') || (a.toLowerCase() === '--runinband'))) { + args.push(`--max-workers=${config.maxConcurrentWorkers}`); + } config.logger.info(config.logger.magenta(`jest args: ${args.join(' ')}`)); diff --git a/src/testing/jest/jest-preprocessor.ts b/src/testing/jest/jest-preprocessor.ts index fa5b9f34d85..fcda5e1f66a 100644 --- a/src/testing/jest/jest-preprocessor.ts +++ b/src/testing/jest/jest-preprocessor.ts @@ -3,9 +3,14 @@ import { loadTypeScriptDiagnostic, normalizePath } from '@utils'; import { transpile } from '../test-transpile'; import { ts } from '@stencil/core/compiler'; +type StencilTestingOptions = { useESModules: boolean; }; // TODO(STENCIL-306): Remove support for earlier versions of Jest type Jest26CacheKeyOptions = { instrument: boolean; rootDir: string }; -type Jest26Config = { instrument: boolean; rootDir: string }; +interface Jest26Config { + instrument: boolean; + rootDir: string; + globals?: { stencil?: { testing?: StencilTestingOptions } }; +}; type Jest27TransformOptions = { config: Jest26Config }; /** @@ -69,10 +74,13 @@ export const jestPreprocessor = { transformOptions = jestConfig.config; } - if (shouldTransform(sourcePath, sourceText)) { + const useESModules = transformOptions.globals?.stencil?.testing?.useESModules ?? false; + + if (shouldTransform(sourcePath, sourceText, useESModules)) { const opts: TranspileOptions = { file: sourcePath, currentDirectory: transformOptions.rootDir, + module: useESModules ? 'esm' : 'cjs' }; const tsCompilerOptions: ts.CompilerOptions = getCompilerOptions(transformOptions.rootDir); @@ -217,9 +225,11 @@ function getCompilerOptions(rootDir: string): ts.CompilerOptions | null { * Determines if a file should be transformed prior to being consumed by Jest, based on the file name and its contents * @param filePath the path of the file * @param sourceText the contents of the file + * @param useESModules when `true`, transform output uses ES Modules; files that are already ESM are + * not transformed. * @returns `true` if the file should be transformed, `false` otherwise */ -export function shouldTransform(filePath: string, sourceText: string): boolean { +export function shouldTransform(filePath: string, sourceText: string, useESModules: boolean): boolean { const ext = filePath.split('.').pop().toLowerCase().split('?')[0]; if (ext === 'ts' || ext === 'tsx' || ext === 'jsx') { @@ -227,17 +237,22 @@ export function shouldTransform(filePath: string, sourceText: string): boolean { return true; } if (ext === 'mjs') { - // es module extensions - return true; + // es module extensions - transform when useESModules is false + return !useESModules; } if (ext === 'js') { - // there may be false positives here - // but worst case scenario a commonjs file is transpiled to commonjs - if (sourceText.includes('import ') || sourceText.includes('import.') || sourceText.includes('import(')) { - return true; + if (useESModules) { + return sourceText.includes('require('); } - if (sourceText.includes('export ')) { - return true; + else { + // there may be false positives here + // but worst case scenario a commonjs file is transpiled to commonjs + if (sourceText.includes('import ') || sourceText.includes('import.') || sourceText.includes('import(')) { + return true; + } + if (sourceText.includes('export ')) { + return true; + } } } if (ext === 'css') { diff --git a/src/testing/jest/jest-setup-test-framework.ts b/src/testing/jest/jest-setup-test-framework.ts index a0d369f4710..b16583fd6bd 100644 --- a/src/testing/jest/jest-setup-test-framework.ts +++ b/src/testing/jest/jest-setup-test-framework.ts @@ -43,6 +43,7 @@ export function jestSetupTestFramework() { global.resourcesUrl = '/build'; }); + if (globalThis.jasmine) { const jasmineEnv = (jasmine as any).getEnv(); if (jasmineEnv != null) { jasmineEnv.addReporter({ @@ -50,6 +51,7 @@ export function jestSetupTestFramework() { global.currentSpec = spec; }, }); + } } global.screenshotDescriptions = new Set(); diff --git a/src/testing/jest/test/jest-preprocessor.spec.ts b/src/testing/jest/test/jest-preprocessor.spec.ts index 8e8c7a58d1e..343868b9ac2 100644 --- a/src/testing/jest/test/jest-preprocessor.spec.ts +++ b/src/testing/jest/test/jest-preprocessor.spec.ts @@ -1,21 +1,47 @@ import { shouldTransform } from '../jest-preprocessor'; describe('jest preprocessor', () => { - it('shouldTransform', () => { - expect(shouldTransform('file.ts', '')).toBe(true); - expect(shouldTransform('file.d.ts', '')).toBe(true); - expect(shouldTransform('file.tsx', '')).toBe(true); - expect(shouldTransform('file.jsx', '')).toBe(true); - expect(shouldTransform('file.mjs', '')).toBe(true); - expect(shouldTransform('file.css', '')).toBe(true); - expect(shouldTransform('file.CsS', '')).toBe(true); - expect(shouldTransform('file.css?tag=my-cmp', '')).toBe(true); + + describe('in CJS mode', () => { + it('shouldTransform', () => { + expect(shouldTransform('file.ts', '', false)).toBe(true); + expect(shouldTransform('file.d.ts', '', false)).toBe(true); + expect(shouldTransform('file.tsx', '', false)).toBe(true); + expect(shouldTransform('file.jsx', '', false)).toBe(true); + expect(shouldTransform('file.mjs', '', false)).toBe(true); + expect(shouldTransform('file.cjs', '', false)).toBe(false); + expect(shouldTransform('file.css', '', false)).toBe(true); + expect(shouldTransform('file.CsS', '', false)).toBe(true); + expect(shouldTransform('file.css?tag=my-cmp', '', false)).toBe(true); + }); + + it('shouldTransform js ext with es module imports/exports', () => { + expect(shouldTransform('file.js', 'import {} from "./file";', false)).toBe(true); + expect(shouldTransform('file.js', 'import.meta.url', false)).toBe(true); + expect(shouldTransform('file.js', 'export * from "./file";', false)).toBe(true); + expect(shouldTransform('file.js', 'console.log("hi")', false)).toBe(false); + }); }); - it('shouldTransform js ext with es module imports/exports', () => { - expect(shouldTransform('file.js', 'import {} from "./file";')).toBe(true); - expect(shouldTransform('file.js', 'import.meta.url')).toBe(true); - expect(shouldTransform('file.js', 'export * from "./file";')).toBe(true); - expect(shouldTransform('file.js', 'console.log("hi")')).toBe(false); + describe('in ESM mode', () => { + it('shouldTransform', () => { + expect(shouldTransform('file.ts', '', true)).toBe(true); + expect(shouldTransform('file.d.ts', '', true)).toBe(true); + expect(shouldTransform('file.tsx', '', true)).toBe(true); + expect(shouldTransform('file.jsx', '', true)).toBe(true); + expect(shouldTransform('file.mjs', '', true)).toBe(false); + expect(shouldTransform('file.cjs', '', true)).toBe(false); + expect(shouldTransform('file.css', '', true)).toBe(true); + expect(shouldTransform('file.CsS', '', true)).toBe(true); + expect(shouldTransform('file.css?tag=my-cmp', '', true)).toBe(true); + }); + + it('shouldTransform returns false for js ext with es module imports/exports', () => { + expect(shouldTransform('file.js', 'import {} from "./file";', true)).toBe(false); + expect(shouldTransform('file.js', 'import.meta.url', true)).toBe(false); + expect(shouldTransform('file.js', 'export * from "./file";', true)).toBe(false); + expect(shouldTransform('file.js', 'console.log("hi")', true)).toBe(false); + }); }); + }); diff --git a/src/testing/test-transpile.ts b/src/testing/test-transpile.ts index 83ffe0bdd8d..dbefe398953 100644 --- a/src/testing/test-transpile.ts +++ b/src/testing/test-transpile.ts @@ -9,7 +9,6 @@ export function transpile(input: string, opts: TranspileOptions = {}): Transpile componentMetadata: 'compilerstatic', coreImportPath: isString(opts.coreImportPath) ? opts.coreImportPath : '@stencil/core/internal/testing', currentDirectory: opts.currentDirectory || process.cwd(), - module: 'cjs', // always use commonjs since we're in a node environment proxy: null, sourceMap: 'inline', style: null, diff --git a/test/jest-esm/jest.config.cjs b/test/jest-esm/jest.config.cjs new file mode 100644 index 00000000000..0d42cb59173 --- /dev/null +++ b/test/jest-esm/jest.config.cjs @@ -0,0 +1,38 @@ +/** + * Jest config for running jest directly (using `node jest -c .../jest.config.js`, not using `node stencil test`). + * This approach can be used when stencil component tests are used alongside other framework tests, eg + * using [Jest projects]{@link https://jestjs.io/docs/configuration#projects-arraystring--projectconfig}. + */ + +module.exports = { + displayName: 'Jest tests using ESM', + // preset: '../../testing/jest-preset.js', + // testRunner: 'jest-jasmine2', + globals: { + stencil: { + testing: { + useESModules: true + } + } + }, + setupFilesAfterEnv: [ + '/../../testing/jest-setuptestframework.js' + ], + moduleDirectories: [ + '../../node_modules' + ], + moduleNameMapper: { + '^@stencil/core/testing$': '/../../testing/index.js', + '^@stencil/core$': '/../../internal/testing/index.js', + '^@stencil/core/internal/app-data$': '/../../internal/app-data/index.cjs', + '^@stencil/core/internal/app-globals$': '/../../internal/app-globals/index.js', + '^@stencil/core/mock-doc$': '/../../mock-doc/index.cjs', + '^@stencil/core/sys$': '/../../sys/node/index.js', + '^@stencil/core$': '/../../internal/testing/index.js', + '^@stencil/core/internal(.*)$': '/../../internal$1' + }, + extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'], + transform: { + '^.+\\.(ts|tsx|jsx|css)$': '../../testing/jest-preprocessor.js' + }, +}; diff --git a/test/jest-esm/package.json b/test/jest-esm/package.json new file mode 100644 index 00000000000..bae6dfe4794 --- /dev/null +++ b/test/jest-esm/package.json @@ -0,0 +1,10 @@ +{ + "name": "@stencil/jest-esm", + "private": true, + "type": "module", + "scripts": { + "build": "node ../../bin/stencil build", + "test": "node ../../bin/stencil test --spec", + "jest": "node --experimental-vm-modules --no-warnings ../../node_modules/jest-cli/bin/jest.js --maxWorkers=3" + } +} diff --git a/test/jest-esm/src/components/mixed/mixed.spec.ts b/test/jest-esm/src/components/mixed/mixed.spec.ts new file mode 100644 index 00000000000..2e17d2b0b88 --- /dev/null +++ b/test/jest-esm/src/components/mixed/mixed.spec.ts @@ -0,0 +1,25 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { MyMixed } from './mixed'; +import { expect } from '@jest/globals'; + +describe('my-mixed', () => { + it('renders', async () => { + const page = await newSpecPage({ + components: [MyMixed], + html: '', + }); + expect(page.root).toEqualHtml(` + + + deep esm!deep ts! + + + deep cjs!Cannot require(ESM file) from CJS file + + + deep esm!deep ts! + + + `); + }); +}); diff --git a/test/jest-esm/src/components/mixed/mixed.tsx b/test/jest-esm/src/components/mixed/mixed.tsx new file mode 100644 index 00000000000..b6f667d4fbb --- /dev/null +++ b/test/jest-esm/src/components/mixed/mixed.tsx @@ -0,0 +1,21 @@ +// @ts-nocheck +import { Component, Host, h } from '@stencil/core'; +import deepEsm from '../utils/as-js-esm'; +import deepCjs from '../utils/as-cjs.cjs'; +import deepTs from '../utils/as-ts'; + +@Component({ + tag: 'my-mixed', + shadow: false, +}) +export class MyMixed { + render() { + return ( + + {deepEsm()} + {deepCjs()} + {deepTs()} + + ); + } +} diff --git a/test/jest-esm/src/components/simple/simple.spec.ts b/test/jest-esm/src/components/simple/simple.spec.ts new file mode 100644 index 00000000000..a24a865332c --- /dev/null +++ b/test/jest-esm/src/components/simple/simple.spec.ts @@ -0,0 +1,24 @@ +import { newSpecPage } from '@stencil/core/testing'; +import { MySimple } from './simple'; +import { expect } from '@jest/globals'; + +/// + +describe('my-simple', () => { + it('renders', async () => { + const page = await newSpecPage({ + components: [MySimple], + html: '', + }); + + // Currently this ugly cast is required for expect extensions in ESM - fix should be coming soon. + // https://github.com/facebook/jest/issues/12267 + (expect(page.root) as unknown as jest.JestMatchers).toEqualHtml(` + + + simple! + + + `); + }); +}); diff --git a/test/jest-esm/src/components/simple/simple.tsx b/test/jest-esm/src/components/simple/simple.tsx new file mode 100644 index 00000000000..4e8cfd51803 --- /dev/null +++ b/test/jest-esm/src/components/simple/simple.tsx @@ -0,0 +1,11 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'my-simple', + shadow: false, +}) +export class MySimple { + render() { + return simple!; + } +} diff --git a/test/jest-esm/src/components/utils/as-cjs.cjs b/test/jest-esm/src/components/utils/as-cjs.cjs new file mode 100644 index 00000000000..b78840d737b --- /dev/null +++ b/test/jest-esm/src/components/utils/as-cjs.cjs @@ -0,0 +1,8 @@ +const { deep: js } = require('./deep-cjs.cjs'); + +// Must use import to load ES Modules, unless we patch in a require() method +// (require ESM file not supported by node) +// const { deep: ts } = require('./deep-ts'); +const ts = 'Cannot require(ESM file) from CJS file'; + +module.exports = function () { return [js, ts]; } diff --git a/test/jest-esm/src/components/utils/as-js-esm.js b/test/jest-esm/src/components/utils/as-js-esm.js new file mode 100644 index 00000000000..2c2df88f2dd --- /dev/null +++ b/test/jest-esm/src/components/utils/as-js-esm.js @@ -0,0 +1,4 @@ +import { deep as js } from './deep-js-esm'; +import { deep as ts } from './deep-ts'; + +export default () => [js, ts]; diff --git a/test/jest-esm/src/components/utils/as-mjs.mjs b/test/jest-esm/src/components/utils/as-mjs.mjs new file mode 100644 index 00000000000..fd93aac1730 --- /dev/null +++ b/test/jest-esm/src/components/utils/as-mjs.mjs @@ -0,0 +1,5 @@ +import { deep as js } from './deep-js-esm'; +import { deep as ts } from './deep-ts'; +import { deep as mjs } from './deep-mjs'; + +export default () => [js, ts, mjs]; diff --git a/test/jest-esm/src/components/utils/as-ts.ts b/test/jest-esm/src/components/utils/as-ts.ts new file mode 100644 index 00000000000..2c2df88f2dd --- /dev/null +++ b/test/jest-esm/src/components/utils/as-ts.ts @@ -0,0 +1,4 @@ +import { deep as js } from './deep-js-esm'; +import { deep as ts } from './deep-ts'; + +export default () => [js, ts]; diff --git a/test/jest-esm/src/components/utils/deep-cjs.cjs b/test/jest-esm/src/components/utils/deep-cjs.cjs new file mode 100644 index 00000000000..e8ac3293464 --- /dev/null +++ b/test/jest-esm/src/components/utils/deep-cjs.cjs @@ -0,0 +1,3 @@ +const deep = 'deep cjs!'; + +exports.deep = deep; diff --git a/test/jest-esm/src/components/utils/deep-js-esm.js b/test/jest-esm/src/components/utils/deep-js-esm.js new file mode 100644 index 00000000000..3cc26cf1b5c --- /dev/null +++ b/test/jest-esm/src/components/utils/deep-js-esm.js @@ -0,0 +1,3 @@ +const deep = 'deep esm!'; + +export { deep }; diff --git a/test/jest-esm/src/components/utils/deep-mjs.mjs b/test/jest-esm/src/components/utils/deep-mjs.mjs new file mode 100644 index 00000000000..003ea8d365e --- /dev/null +++ b/test/jest-esm/src/components/utils/deep-mjs.mjs @@ -0,0 +1,3 @@ +const deep = 'deep mjs!'; + +export { deep }; diff --git a/test/jest-esm/src/components/utils/deep-ts.ts b/test/jest-esm/src/components/utils/deep-ts.ts new file mode 100644 index 00000000000..516e4809bf0 --- /dev/null +++ b/test/jest-esm/src/components/utils/deep-ts.ts @@ -0,0 +1,3 @@ +const deep = 'deep ts!'; + +export { deep }; diff --git a/test/jest-esm/stencil.config.ts b/test/jest-esm/stencil.config.ts new file mode 100644 index 00000000000..000f1855df9 --- /dev/null +++ b/test/jest-esm/stencil.config.ts @@ -0,0 +1,7 @@ +import { Config } from '../../internal'; + +export const config: Config = { + testing: { + useESModules: true + } +}; diff --git a/test/jest-esm/tsconfig.json b/test/jest-esm/tsconfig.json new file mode 100644 index 00000000000..8d7577fd064 --- /dev/null +++ b/test/jest-esm/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "allowSyntheticDefaultImports": true, + "allowJs": true, + "allowUnreachableCode": false, + "declaration": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "jsxFactory": "h", + "lib": ["dom", "es2017"], + "module": "esnext", + "moduleResolution": "node", + "pretty": true, + "target": "es2020", + "baseUrl": ".", + "paths": { + "@stencil/core": ["../../internal"], + "@stencil/core/internal": ["../../internal"], + "@stencil/core/testing": ["../../testing"] + } + }, + "include": ["src"] +} From eab4590f15f1fe6832c54e5a381d145080d13513 Mon Sep 17 00:00:00 2001 From: John Crim Date: Thu, 24 Feb 2022 11:38:22 -0800 Subject: [PATCH 2/6] feat(stencil test esm): stencil test support for running ESM tests --- src/testing/jest/jest-config.ts | 12 ++++++++++++ src/testing/jest/jest-runner.ts | 2 +- src/testing/jest/jest-setup-test-framework.ts | 18 ++++++++++-------- test/jest-esm/jest.config.cjs | 19 +------------------ test/jest-esm/package.json | 2 +- test/jest-esm/stencil.config.ts | 5 ++++- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/testing/jest/jest-config.ts b/src/testing/jest/jest-config.ts index 61481f21346..cebe1eafafd 100644 --- a/src/testing/jest/jest-config.ts +++ b/src/testing/jest/jest-config.ts @@ -108,6 +108,18 @@ export function buildJestConfig(config: d.Config): string { if (stencilConfigTesting.coverageThreshold) { jestConfig.coverageThreshold = stencilConfigTesting.coverageThreshold; } + if (stencilConfigTesting.useESModules) { + jestConfig.globals = { + stencil: { + testing: { + useESModules: true + } + } + } + if (!jestConfig.extensionsToTreatAsEsm) { + jestConfig.extensionsToTreatAsEsm = ['.ts', '.tsx', '.jsx']; + } + } if (isString(stencilConfigTesting.globalSetup)) { jestConfig.globalSetup = stencilConfigTesting.globalSetup; } diff --git a/src/testing/jest/jest-runner.ts b/src/testing/jest/jest-runner.ts index 5ed8ca39787..6a23dfdfc41 100644 --- a/src/testing/jest/jest-runner.ts +++ b/src/testing/jest/jest-runner.ts @@ -22,7 +22,7 @@ export async function runJest(config: d.Config, env: d.E2EProcessEnv) { } config.logger.debug(`default timeout: ${env.__STENCIL_DEFAULT_TIMEOUT__}`); - // build up our args from our already know list of args in the config + // build up our args from our already known list of args in the config const jestArgv = buildJestArgv(config); // build up the project paths, which is basically the app's root dir const projects = getProjectListFromCLIArgs(config, jestArgv); diff --git a/src/testing/jest/jest-setup-test-framework.ts b/src/testing/jest/jest-setup-test-framework.ts index b16583fd6bd..4d69e70608e 100644 --- a/src/testing/jest/jest-setup-test-framework.ts +++ b/src/testing/jest/jest-setup-test-framework.ts @@ -44,13 +44,13 @@ export function jestSetupTestFramework() { }); if (globalThis.jasmine) { - const jasmineEnv = (jasmine as any).getEnv(); - if (jasmineEnv != null) { - jasmineEnv.addReporter({ - specStarted: (spec: any) => { - global.currentSpec = spec; - }, - }); + const jasmineEnv = (jasmine as any).getEnv(); + if (jasmineEnv != null) { + jasmineEnv.addReporter({ + specStarted: (spec: any) => { + global.currentSpec = spec; + }, + }); } } @@ -61,7 +61,9 @@ export function jestSetupTestFramework() { if (typeof env.__STENCIL_DEFAULT_TIMEOUT__ === 'string') { const time = parseInt(env.__STENCIL_DEFAULT_TIMEOUT__, 10); jest.setTimeout(time * 1.5); - jasmine.DEFAULT_TIMEOUT_INTERVAL = time; + if (globalThis.jasmine) { + jasmine.DEFAULT_TIMEOUT_INTERVAL = time; + } } if (typeof env.__STENCIL_ENV__ === 'string') { const stencilEnv = JSON.parse(env.__STENCIL_ENV__); diff --git a/test/jest-esm/jest.config.cjs b/test/jest-esm/jest.config.cjs index 0d42cb59173..5d2b5f524d7 100644 --- a/test/jest-esm/jest.config.cjs +++ b/test/jest-esm/jest.config.cjs @@ -6,8 +6,7 @@ module.exports = { displayName: 'Jest tests using ESM', - // preset: '../../testing/jest-preset.js', - // testRunner: 'jest-jasmine2', + preset: '../../testing/jest-preset.js', globals: { stencil: { testing: { @@ -15,24 +14,8 @@ module.exports = { } } }, - setupFilesAfterEnv: [ - '/../../testing/jest-setuptestframework.js' - ], moduleDirectories: [ '../../node_modules' ], - moduleNameMapper: { - '^@stencil/core/testing$': '/../../testing/index.js', - '^@stencil/core$': '/../../internal/testing/index.js', - '^@stencil/core/internal/app-data$': '/../../internal/app-data/index.cjs', - '^@stencil/core/internal/app-globals$': '/../../internal/app-globals/index.js', - '^@stencil/core/mock-doc$': '/../../mock-doc/index.cjs', - '^@stencil/core/sys$': '/../../sys/node/index.js', - '^@stencil/core$': '/../../internal/testing/index.js', - '^@stencil/core/internal(.*)$': '/../../internal$1' - }, extensionsToTreatAsEsm: ['.ts', '.tsx', '.jsx'], - transform: { - '^.+\\.(ts|tsx|jsx|css)$': '../../testing/jest-preprocessor.js' - }, }; diff --git a/test/jest-esm/package.json b/test/jest-esm/package.json index bae6dfe4794..b4d1243d458 100644 --- a/test/jest-esm/package.json +++ b/test/jest-esm/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "node ../../bin/stencil build", - "test": "node ../../bin/stencil test --spec", + "test": "node --experimental-vm-modules --no-warnings ../../bin/stencil test --spec", "jest": "node --experimental-vm-modules --no-warnings ../../node_modules/jest-cli/bin/jest.js --maxWorkers=3" } } diff --git a/test/jest-esm/stencil.config.ts b/test/jest-esm/stencil.config.ts index 000f1855df9..006938965ee 100644 --- a/test/jest-esm/stencil.config.ts +++ b/test/jest-esm/stencil.config.ts @@ -2,6 +2,9 @@ import { Config } from '../../internal'; export const config: Config = { testing: { - useESModules: true + useESModules: true, + moduleDirectories: [ + '../../node_modules' + ] } }; From b014907784b9149ea965a9ccf9960da157378e3e Mon Sep 17 00:00:00 2001 From: John Crim Date: Thu, 24 Feb 2022 12:55:06 -0800 Subject: [PATCH 3/6] fix(test): jest config test error Previous changes introduced a change in command-line arg processing. --- src/testing/jest/jest-config.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/testing/jest/jest-config.ts b/src/testing/jest/jest-config.ts index cebe1eafafd..59685612169 100644 --- a/src/testing/jest/jest-config.ts +++ b/src/testing/jest/jest-config.ts @@ -38,6 +38,15 @@ function getLegacyJestOptions(): Record { }; } +/** @returns true if the argument sets max-workers or runInBand. */ +function argSetsWorkers(arg: string) { + const lowerCased = arg.toLowerCase(); + return (lowerCased === '-i') + || (lowerCased === '--runinband') + || lowerCased.startsWith('--max-workers') + || lowerCased.startsWith('--maxworkers'); +} + /** * Builds the `argv` to be used when programmatically invoking the Jest CLI * @param config the Stencil config to use while generating Jest CLI arguments @@ -48,11 +57,13 @@ export function buildJestArgv(config: d.Config): Config.Argv { const args = [...config.flags.unknownArgs.slice(), ...config.flags.knownArgs.slice()]; - if (config.flags.devtools) { - args.push('--runInBand'); - } - else if (!args.some((a) => a.startsWith('--max-workers') || (a === '-i') || (a.toLowerCase() === '--runinband'))) { - args.push(`--max-workers=${config.maxConcurrentWorkers}`); + if (!args.some(argSetsWorkers)) { + if (config.flags.devtools) { + args.push('--runInBand'); + } + else { + args.push(`--max-workers=${config.maxConcurrentWorkers}`); + } } config.logger.info(config.logger.magenta(`jest args: ${args.join(' ')}`)); From ac9fc4a7edb8de83ba36b95345212020aafdd911 Mon Sep 17 00:00:00 2001 From: John Crim Date: Thu, 24 Feb 2022 13:08:57 -0800 Subject: [PATCH 4/6] test(jest CLI processing): Add tests to validate fixed bug The previous bug is that --max-workers should not be set when --runInBand is used. --- src/testing/jest/test/jest-config.spec.ts | 22 ++++++++++++++++++++++ test/jest-esm/package.json | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/testing/jest/test/jest-config.spec.ts b/src/testing/jest/test/jest-config.spec.ts index 23dd37b9aff..51ef0f706ac 100644 --- a/src/testing/jest/test/jest-config.spec.ts +++ b/src/testing/jest/test/jest-config.spec.ts @@ -60,6 +60,28 @@ describe('jest-config', () => { expect(jestArgv.maxWorkers).toBe(2); }); + it('--maxWorkers arg is set from config', () => { + const config = mockConfig(); + config.testing = {}; + config.maxConcurrentWorkers = 3; + config.flags = parseFlags(['test'], config.sys); + + const jestArgv = buildJestArgv(config); + expect(jestArgv.maxWorkers).toBe(3); + }); + + it('--maxWorkers arg is not set from config when --runInBand is used', () => { + const config = mockConfig(); + config.testing = {}; + config.maxConcurrentWorkers = 3; + + const args = ['test', '--runInBand']; + config.flags = parseFlags(args, config.sys); + + const jestArgv = buildJestArgv(config); + expect(jestArgv.maxWorkers).toBeUndefined(); + }); + it('pass --ci arg to jest', () => { const args = ['test', '--ci']; const config = mockConfig(); diff --git a/test/jest-esm/package.json b/test/jest-esm/package.json index b4d1243d458..dbadf777f82 100644 --- a/test/jest-esm/package.json +++ b/test/jest-esm/package.json @@ -5,6 +5,6 @@ "scripts": { "build": "node ../../bin/stencil build", "test": "node --experimental-vm-modules --no-warnings ../../bin/stencil test --spec", - "jest": "node --experimental-vm-modules --no-warnings ../../node_modules/jest-cli/bin/jest.js --maxWorkers=3" + "jest": "node --experimental-vm-modules --no-warnings ../../node_modules/jest/bin/jest.js --maxWorkers=3" } } From 52bef1db19c93ea6bfc053cf8c43d00b1dedb0a8 Mon Sep 17 00:00:00 2001 From: John Crim Date: Thu, 24 Feb 2022 13:14:09 -0800 Subject: [PATCH 5/6] chore(formatting): prettier --- src/testing/jest/jest-config.ts | 21 ++++++++++--------- src/testing/jest/jest-preprocessor.ts | 9 ++++---- .../jest/test/jest-preprocessor.spec.ts | 2 -- test/jest-esm/stencil.config.ts | 6 ++---- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/testing/jest/jest-config.ts b/src/testing/jest/jest-config.ts index 59685612169..30a469809ad 100644 --- a/src/testing/jest/jest-config.ts +++ b/src/testing/jest/jest-config.ts @@ -41,10 +41,12 @@ function getLegacyJestOptions(): Record { /** @returns true if the argument sets max-workers or runInBand. */ function argSetsWorkers(arg: string) { const lowerCased = arg.toLowerCase(); - return (lowerCased === '-i') - || (lowerCased === '--runinband') - || lowerCased.startsWith('--max-workers') - || lowerCased.startsWith('--maxworkers'); + return ( + lowerCased === '-i' || + lowerCased === '--runinband' || + lowerCased.startsWith('--max-workers') || + lowerCased.startsWith('--maxworkers') + ); } /** @@ -60,8 +62,7 @@ export function buildJestArgv(config: d.Config): Config.Argv { if (!args.some(argSetsWorkers)) { if (config.flags.devtools) { args.push('--runInBand'); - } - else { + } else { args.push(`--max-workers=${config.maxConcurrentWorkers}`); } } @@ -123,10 +124,10 @@ export function buildJestConfig(config: d.Config): string { jestConfig.globals = { stencil: { testing: { - useESModules: true - } - } - } + useESModules: true, + }, + }, + }; if (!jestConfig.extensionsToTreatAsEsm) { jestConfig.extensionsToTreatAsEsm = ['.ts', '.tsx', '.jsx']; } diff --git a/src/testing/jest/jest-preprocessor.ts b/src/testing/jest/jest-preprocessor.ts index fcda5e1f66a..c1951297b32 100644 --- a/src/testing/jest/jest-preprocessor.ts +++ b/src/testing/jest/jest-preprocessor.ts @@ -3,14 +3,14 @@ import { loadTypeScriptDiagnostic, normalizePath } from '@utils'; import { transpile } from '../test-transpile'; import { ts } from '@stencil/core/compiler'; -type StencilTestingOptions = { useESModules: boolean; }; +type StencilTestingOptions = { useESModules: boolean }; // TODO(STENCIL-306): Remove support for earlier versions of Jest type Jest26CacheKeyOptions = { instrument: boolean; rootDir: string }; interface Jest26Config { instrument: boolean; rootDir: string; globals?: { stencil?: { testing?: StencilTestingOptions } }; -}; +} type Jest27TransformOptions = { config: Jest26Config }; /** @@ -80,7 +80,7 @@ export const jestPreprocessor = { const opts: TranspileOptions = { file: sourcePath, currentDirectory: transformOptions.rootDir, - module: useESModules ? 'esm' : 'cjs' + module: useESModules ? 'esm' : 'cjs', }; const tsCompilerOptions: ts.CompilerOptions = getCompilerOptions(transformOptions.rootDir); @@ -243,8 +243,7 @@ export function shouldTransform(filePath: string, sourceText: string, useESModul if (ext === 'js') { if (useESModules) { return sourceText.includes('require('); - } - else { + } else { // there may be false positives here // but worst case scenario a commonjs file is transpiled to commonjs if (sourceText.includes('import ') || sourceText.includes('import.') || sourceText.includes('import(')) { diff --git a/src/testing/jest/test/jest-preprocessor.spec.ts b/src/testing/jest/test/jest-preprocessor.spec.ts index 343868b9ac2..c4d401563e1 100644 --- a/src/testing/jest/test/jest-preprocessor.spec.ts +++ b/src/testing/jest/test/jest-preprocessor.spec.ts @@ -1,7 +1,6 @@ import { shouldTransform } from '../jest-preprocessor'; describe('jest preprocessor', () => { - describe('in CJS mode', () => { it('shouldTransform', () => { expect(shouldTransform('file.ts', '', false)).toBe(true); @@ -43,5 +42,4 @@ describe('jest preprocessor', () => { expect(shouldTransform('file.js', 'console.log("hi")', true)).toBe(false); }); }); - }); diff --git a/test/jest-esm/stencil.config.ts b/test/jest-esm/stencil.config.ts index 006938965ee..9d6fd46263f 100644 --- a/test/jest-esm/stencil.config.ts +++ b/test/jest-esm/stencil.config.ts @@ -3,8 +3,6 @@ import { Config } from '../../internal'; export const config: Config = { testing: { useESModules: true, - moduleDirectories: [ - '../../node_modules' - ] - } + moduleDirectories: ['../../node_modules'], + }, }; From 6d55e845524a885a30ff12589d77ccd83e392482 Mon Sep 17 00:00:00 2001 From: John Crim Date: Thu, 24 Feb 2022 14:02:19 -0800 Subject: [PATCH 6/6] test(jest config): validate generated jest config --- src/testing/jest/jest-config.ts | 14 ++++++------ src/testing/jest/test/jest-config.spec.ts | 28 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/testing/jest/jest-config.ts b/src/testing/jest/jest-config.ts index 30a469809ad..5f003d09ad6 100644 --- a/src/testing/jest/jest-config.ts +++ b/src/testing/jest/jest-config.ts @@ -120,14 +120,14 @@ export function buildJestConfig(config: d.Config): string { if (stencilConfigTesting.coverageThreshold) { jestConfig.coverageThreshold = stencilConfigTesting.coverageThreshold; } - if (stencilConfigTesting.useESModules) { - jestConfig.globals = { - stencil: { - testing: { - useESModules: true, - }, + jestConfig.globals = { + stencil: { + testing: { + useESModules: !!stencilConfigTesting.useESModules, }, - }; + }, + }; + if (stencilConfigTesting.useESModules) { if (!jestConfig.extensionsToTreatAsEsm) { jestConfig.extensionsToTreatAsEsm = ['.ts', '.tsx', '.jsx']; } diff --git a/src/testing/jest/test/jest-config.spec.ts b/src/testing/jest/test/jest-config.spec.ts index 51ef0f706ac..ffe99daae7a 100644 --- a/src/testing/jest/test/jest-config.spec.ts +++ b/src/testing/jest/test/jest-config.spec.ts @@ -200,4 +200,32 @@ describe('jest-config', () => { expect(parsedConfig.collectCoverageFrom).toHaveLength(1); expect(parsedConfig.collectCoverageFrom[0]).toBe('**/*.+(ts|tsx)'); }); + + it('useESModules: true sets ESM jest config', () => { + const config = mockConfig(); + config.testing = { + useESModules: true, + }; + config.flags = parseFlags(['test'], config.sys); + + const jestArgv = buildJestArgv(config); + const parsedConfig = JSON.parse(jestArgv.config) as d.JestConfig; + + expect(parsedConfig.extensionsToTreatAsEsm).toEqual(['.ts', '.tsx', '.jsx']); + expect(parsedConfig.globals.stencil.testing.useESModules).toBe(true); + }); + + it('useESModules: false does not set ESM jest config', () => { + const config = mockConfig(); + config.testing = { + useESModules: false, + }; + config.flags = parseFlags(['test'], config.sys); + + const jestArgv = buildJestArgv(config); + const parsedConfig = JSON.parse(jestArgv.config) as d.JestConfig; + + expect(parsedConfig.extensionsToTreatAsEsm).toBeUndefined(); + expect(parsedConfig.globals.stencil.testing.useESModules).toBe(false); + }); });