Skip to content

refactor(jest): detect jest version for testing#4883

Merged
rwaskiewicz merged 1 commit intomainfrom
rwaskiewicz/jest/step-2-get-version
Oct 11, 2023
Merged

refactor(jest): detect jest version for testing#4883
rwaskiewicz merged 1 commit intomainfrom
rwaskiewicz/jest/step-2-get-version

Conversation

@rwaskiewicz
Copy link
Member

@rwaskiewicz rwaskiewicz commented Oct 2, 2023

What is the current behavior?

stencil will always assume that jest v27 is being used for testing.
this is a result of years of using the same files for all jest infrastructure for end users.
while this pr does not do away with always picking jest v27, it gets us one step closer

GitHub Issue Number: N/A

What is the new behavior?

add a new abstraction layer to stencil's jest testing configuration/running such that the version of jest is detected when jest is run. based on the version that is detected, defer to the correct piece of versioned infrastructure to setup and run tests.

in #4847, the directory structure of the testing submodule was updated to look something like:

├── src/testing/jest/
│   └── jest-27-and-under
│       ├── jest-config.ts                 
│       ├── jest-environment.ts
│       ├── jest-preprocessor.ts
│       ├── jest-runner.ts
│       ├── jest-screenshot.ts
│       ├── jest-serializer.ts
│       ├── jest-setup-test-framework.ts
│       ├── node_modules/
│       ├── package.json                   
│       └── test
└── package.json                            

this commit introduces the following infrastructue to dispatch to jest-27-and-under at runtime:

  ├── src/testing/jest/
  │   └── jest-27-and-under
  │       ├── jest-config.ts                
  │       ├── jest-environment.ts
+ │       ├── jest-facade.ts                  <- Implements base class in `jest-adapter.ts`
  │       ├── jest-preprocessor.ts
  │       ├── jest-runner.ts
  │       ├── jest-screenshot.ts
  │       ├── jest-serializer.ts
  │       ├── jest-setup-test-framework.ts
  │       ├── node_modules/
  │       ├── package.json                    
  │       └── test
+ ├── jest-apis.ts                            <- Interactions with Jest prior to knowing the version
+ ├── jest-facade.ts                         <- Base class all `jest-facade` implementations should follow
+ ├── jest-stencil-connector.ts               <- Routes Jest logic through the correct `jest-facade` implementation
  └── package.json                            

Where:

  • jest-stencil-connector - performs the detection of the current version of jest - dispatching to the correct infrastructure via jest-facade implementations
  • jest-facade - a file containing a base class to define the API by which version-specific implementations can be loaded. a jest v27-specific implementation is included as well in jest-27-and-under/
  • jest-apis - a file containing typings for cases where the jest typings are ambiguous, particularly for when we do not know what the current version of jest is (yet)

the end result of this commit should be nothing to the end user (for now). this work shall be used to help support jest v28+ in the near future (dispatching the the correct version)

Does this introduce a breaking change?

  • Yes
  • No

Testing

Pull down this branch and build it. Make note of the location of the generated tarball:

npm run clean \                   
&& npm ci \
&& npm run build \
&& npm pack

Next, create a new stencil component library with that tarball:

cd /tmp \
&& npm init stencil@latest component jest-test \
&& cd $_ \
&& npm i [PATH_TO_TARBALL]

Stencil Testing should continue to pass for Jest [24,27]:

npm i jest@24 jest-cli@24 @types/jest@24 && npm t -- --no-cache && \
npm i jest@25 jest-cli@25 @types/jest@25 && npm t -- --no-cache && \
npm i jest@26 jest-cli@26 @types/jest@26 && npm t -- --no-cache && \
npm i jest@27 jest-cli@27 @types/jest@27 && npm t -- --no-cache

Next, Stencil should continue to warn if a testing module is not installed:

npm uninstall jest \
&& npm t -- --no-cache # expect error

Other information

@github-actions
Copy link
Contributor

github-actions bot commented Oct 2, 2023

--strictNullChecks error report

Typechecking with --strictNullChecks resulted in 1399 errors on this branch.

That's the same number of errors on main, so at least we're not creating new ones!

reports and statistics

Our most error-prone files
Path Error Count
src/dev-server/index.ts 37
src/mock-doc/serialize-node.ts 36
src/dev-server/server-process.ts 32
src/compiler/build/build-stats.ts 27
src/compiler/output-targets/dist-lazy/generate-lazy-module.ts 25
src/compiler/style/test/optimize-css.spec.ts 23
src/testing/puppeteer/puppeteer-element.ts 23
src/compiler/prerender/prerender-main.ts 22
src/runtime/vdom/vdom-render.ts 20
src/runtime/client-hydrate.ts 19
src/screenshot/connector-base.ts 19
src/compiler/config/test/validate-paths.spec.ts 16
src/dev-server/request-handler.ts 15
src/compiler/prerender/prerender-optimize.ts 14
src/compiler/sys/stencil-sys.ts 14
src/compiler/transpile/transpile-module.ts 14
src/runtime/vdom/vdom-annotations.ts 14
src/sys/node/node-sys.ts 14
src/compiler/build/build-finish.ts 13
src/compiler/prerender/prerender-queue.ts 13
Our most common errors
Typescript Error Code Count
TS2345 424
TS2322 398
TS18048 310
TS18047 100
TS2722 38
TS2532 34
TS2531 23
TS2454 14
TS2352 13
TS2769 10
TS2790 10
TS2538 8
TS2344 5
TS2416 4
TS2493 3
TS18046 2
TS2684 1
TS2488 1
TS2430 1

Unused exports report

There are 12 unused exports on this PR. That's the same number of errors on main, so at least we're not creating new ones!

Unused exports
File Line Identifier
src/runtime/bootstrap-lazy.ts 21 setNonce
src/screenshot/screenshot-fs.ts 18 readScreenshotData
src/testing/testing-utils.ts 198 withSilentWarn
src/utils/index.ts 140 CUSTOM
src/compiler/app-core/app-data.ts 25 BUILD
src/compiler/app-core/app-data.ts 114 Env
src/compiler/app-core/app-data.ts 116 NAMESPACE
src/compiler/fs-watch/fs-watch-rebuild.ts 123 updateCacheFromRebuild
src/compiler/types/validate-primary-package-output-target.ts 62 satisfies
src/compiler/types/validate-primary-package-output-target.ts 62 Record
src/testing/puppeteer/puppeteer-declarations.ts 485 WaitForEventOptions
src/compiler/sys/fetch/write-fetch-success.ts 7 writeFetchSuccessSync

@rwaskiewicz rwaskiewicz force-pushed the rwaskiewicz/jest/step-2-get-version branch from b63824f to 1787469 Compare October 2, 2023 20:23
* Creates a Stencil test runner
* @returns the test runner
*/
export function createTestRunner(): any {
Copy link
Member Author

Choose a reason for hiding this comment

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

This was previously typed as any - JestTestRunner is just a type alias for any at this point, but I saved that piece of tech debt for another day - https://github.com/ionic-team/stencil/pull/4883/files#diff-4b0dc677b3f6e55c5f547f5585915852d874a4a1ff77a6211becc016251521fcR38-R39

* directory Stencil supports.
*/
export abstract class JestAdapter {
// TODO(STENCIL-961): Fix build validation when types are pulled in from `@stencil/core/declarations`
Copy link
Member Author

Choose a reason for hiding this comment

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

Importing from @stencil/core or another subdirectory, the test.dist scripts begin to fail. I'm kicking the can down the road a little here on that, in case a better design reveals itself as I work on v28+ implementatons

* Adapter between this directory's version of Jest and Stencil
*/
export class Jest27StencilAdapter extends JestAdapter {
override getJestCliRunner() {
Copy link
Member Author

Choose a reason for hiding this comment

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

There's minimal JSDoc in this file, as it's expected that they (the docs) are inherited by the ABC (unless there's a good reason to override/append to the the JSDoc, then that should be called out)

@rwaskiewicz rwaskiewicz force-pushed the rwaskiewicz/jest/step-2-get-version branch from 1787469 to 937d140 Compare October 3, 2023 12:32
@rwaskiewicz rwaskiewicz marked this pull request as ready for review October 3, 2023 12:32
@rwaskiewicz rwaskiewicz requested a review from a team as a code owner October 3, 2023 12:32
@rwaskiewicz rwaskiewicz force-pushed the rwaskiewicz/jest/step-2-get-version branch from 937d140 to ccd9e62 Compare October 6, 2023 12:03
Copy link
Contributor

@tanner-reits tanner-reits left a comment

Choose a reason for hiding this comment

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

I'm sure you had similar thoughts on the things I commented on and have reasoning for why we didn't do them or why they might not have been feasible. So, feel free to push back on anything.

Only other thing that feels weird to me is the getCreate... naming convention. I can't explain why, but my brain doesn't like it lol I'll live tho if nobody else feels the same 😂

if (!JEST_ADAPTER) {
const version = getVersion();
if (version <= 27) {
JEST_ADAPTER = new Jest27StencilAdapter();
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought maybe this would be able to resolve the "adapter" based on consistent file structure and naming conventions (and a default export from the file). That too magical?

Copy link
Member Author

Choose a reason for hiding this comment

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

The original prototype actually used something very similar to what you're describing:

class Jest27StencilAdapter extends JestFacade {
  static override async getRunner() {
    return await import('./experimental/jest-27-and-under/jest-runner');
  }
  static override async getScreenshot() {
    return await import('./experimental/jest-27-and-under/jest-screenshot');
  }
}

I found there were a couple shortcomings with this:

  1. Type safety wasn't as "nice" as I wanted it to be (the return type of getRunner is a Promise<> that returns the shape of whatever the export of jest-runner is). I found this to be kind of annoying to work with/harder to understand
  2. It introduced async/await to the the whole call chain, which wasn't strictly necessary
  3. Refactoring (say for instance, I were to hoist 'jest-27-and-under' out of that 'experimental' directory 😉) wasn't very pleasurable either.

By using the Facade pattern in this way, we can be super explicit about what adapters/implementations map to which versions of Jest, avoid some async/await overhead, and have some better (IMO) refactoring ability.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, where did the term "facade" come from for these when we use "adapter" for the actual implementations and definitions?

Copy link
Member Author

Choose a reason for hiding this comment

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

"Facade" comes from our use of the Facade Pattern here - adapter isn't quite right in terms of a classic Gang of Four pattern, but I renamed things so many times that it probably "just made sense" to me at the time. I'll rename these a bit for some better....well, to be better 😆

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, I think I made this a little more cohesive in 2ac845e - LMK what you think

}

jestConfig.testRunner = 'jest-jasmine2';
jestConfig.testRunner = getDefaultJestRunner();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this pull directly from the the v27 adapter rather than the abstract one? I know it evaluates to the same thing, but that would keep the version-specific adapters from "knowing about" the more generic stuff

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure - my only concern here is that the general version has a cache of the adapter, whereas the version specific versions don't. Let me verify this won't run >1 for npx stencil test and I'll circle back

Copy link
Member Author

Choose a reason for hiding this comment

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

Mkay - we can do this 👍 Verified that this isn't called >1, so we don't end up instantiating a new Facade instance each time we test. Updated in 615402c

Comment on lines +20 to +23
type Jest26CacheKeyOptions = { instrument: boolean; rootDir: string };
type Jest26Config = { instrument: boolean; rootDir: string };
type Jest27TransformOptions = { config: Jest26Config };
export type JestPreprocessor = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Another spot where it'd be nice if the abstract layer didn't need to worry about version-specific APIs. Anything we can do with generics here? How are these types consumed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Another spot where it'd be nice if the abstract layer didn't need to worry about version-specific APIs.

Trust me, I wish we could 😆 The problem is that the generic version of our connectors/adapters need to return something. And those something's need to be compatible with Jest.

Anything we can do with generics here? How are these types consumed?

Right now, they're only used by jest-adapter.ts. I think in the near future, we'll end up refactoring this to be less aware of these types. But the exact shape/form hasn't "presented itself" yet - I have a feeling once we get Jest 28 in place, that'll start to become more evident

Copy link
Contributor

@alicewriteswrongs alicewriteswrongs left a comment

Choose a reason for hiding this comment

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

I was able to build this and then run tests in a new project with no problems, so everything seems in working order - just had one sort of structural question about using an ABC vs possible a class or object conforming to an interface. Looks good overall and no real concerns!

/**
* `JestFacade` implementation for communicating between between this directory's version of Jest and Stencil
*/
export class Jest27Stencil extends JestFacade {
Copy link
Contributor

Choose a reason for hiding this comment

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

j/w, are there any advantages / disadvantages either way between the approaches of having an ABC and implementations that extend it (a là JEst27Stencil here) vs an interface that the actual implemetations then implement? classes with the abstract keyword do actually have a runtime representation and can have concrete methods on them - right now it looks like the JestFacade doesn't have any concrete / non-abstract methods on it so would basically be class JestFacade { } after compilation.

Did you give any thought to using an interface to constrain the shape of the facade classes (like Jest27Stencil here) instead of an ABC? Maybe we will need to have some behavior on the base class in the future

anyway just sort of thinking through this as I read the PR

Copy link
Member Author

Choose a reason for hiding this comment

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

I did give it some thought, and originally had gone the ABC route because I did have a few concrete implementations in the base class here - but those have obviously been removed since then (hence your question). It's relatively trivial to convert between the two. I say let's go with an interface for now unless we find we really need it later down the road. Done in 4327cfc

Copy link
Contributor

@tanner-reits tanner-reits left a comment

Choose a reason for hiding this comment

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

LGTM. I think this would be a good candidate for some sort of guide to follow when adding support (and removing support) for Jest versions. Just because we have to add the version-specific adapter code and remember to update the abstract layer to recognize and use the correct versions

@rwaskiewicz
Copy link
Member Author

@tanner-reits

... I think this would be a good candidate for some sort of guide to follow when adding support (and removing support) for Jest versions. Just because we have to add the version-specific adapter code and remember to update the abstract layer to recognize and use the correct versions

That's my current plan - I'll write one up while I do the Jest 28 related stuff, then try to follow it verbatim for Jest 29 😆 🤞

add a new abstraction layer to stencil's jest testing
configuration/running such that the version of jest is detected when
jest is run. based on the version that is detected, defer to the
correct piece of versioned infrastructure to setup and run tests.

in #4847, the directory
structure of the testing submodule was updated to look something like:
```
├── src/testing/jest/
│   └── jest-27-and-under
│       ├── jest-config.ts
│       ├── jest-environment.ts
│       ├── jest-preprocessor.ts
│       ├── jest-runner.ts
│       ├── jest-screenshot.ts
│       ├── jest-serializer.ts
│       ├── jest-setup-test-framework.ts
│       ├── node_modules/
│       ├── package.json
│       └── test
└── package.json
```

this commit introduces the following infrastructue to dispatch to
jest-27-and-under at runtime:

```diff
  ├── src/testing/jest/
  │   └── jest-27-and-under
  │       ├── jest-config.ts
  │       ├── jest-environment.ts
+ │       ├── jest-facade.ts                  <- Implements interface in `../jest-adapter.ts`
  │       ├── jest-preprocessor.ts
  │       ├── jest-runner.ts
  │       ├── jest-screenshot.ts
  │       ├── jest-serializer.ts
  │       ├── jest-setup-test-framework.ts
  │       ├── node_modules/
  │       ├── package.json
  │       └── test
+ ├── jest-apis.ts                            <- Interactions with Jest prior to knowing the version
+ ├── jest-facade.ts                          <- Interface  all `jest-facade` implementations should follow
+ ├── jest-stencil-connector.ts               <- Routes Jest logic through the correct `jest-facade` implementation
  └── package.json
```

Where:
- `jest-stencil-connector` - performs the detection of the current
version of jest - dispatching to the correct infrastructure via
`jest-facade` implementations
- `jest-facade.ts` - a file containing an interface to define the API by
which version-specific implementation can be loaded. the first
implementation for jest 27 and under is included in `jest-27-and-under/`
- `jest-apis` - a file containing typings for cases where the jest
typings are ambiguous, particularly for when we do not know what the
current version of jest is (yet)

the end result of this commit should be nothing to the end user (for
now). this work shall be used to help support jest v28+ in the near
future (dispatching the the correct version)
@rwaskiewicz rwaskiewicz force-pushed the rwaskiewicz/jest/step-2-get-version branch from 4327cfc to 8dc8895 Compare October 11, 2023 19:56
@rwaskiewicz rwaskiewicz enabled auto-merge October 11, 2023 19:59
@rwaskiewicz rwaskiewicz added this pull request to the merge queue Oct 11, 2023
@alicewriteswrongs
Copy link
Contributor

ditto on the upgrade guide - ideally just a README.md in the right spot so you can't miss it

Merged via the queue into main with commit a699cec Oct 11, 2023
@rwaskiewicz rwaskiewicz deleted the rwaskiewicz/jest/step-2-get-version branch October 11, 2023 20:18
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.

3 participants