refactor(jest): detect jest version for testing#4883
Conversation
|
| 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 |
b63824f to
1787469
Compare
| * Creates a Stencil test runner | ||
| * @returns the test runner | ||
| */ | ||
| export function createTestRunner(): any { |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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)
1787469 to
937d140
Compare
937d140 to
ccd9e62
Compare
tanner-reits
left a comment
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- Type safety wasn't as "nice" as I wanted it to be (the return type of
getRunneris aPromise<>that returns the shape of whatever the export ofjest-runneris). I found this to be kind of annoying to work with/harder to understand - It introduced
async/awaitto the the whole call chain, which wasn't strictly necessary - 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.
There was a problem hiding this comment.
Just curious, where did the term "facade" come from for these when we use "adapter" for the actual implementations and definitions?
There was a problem hiding this comment.
"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 😆
There was a problem hiding this comment.
Okay, I think I made this a little more cohesive in 2ac845e - LMK what you think
| } | ||
|
|
||
| jestConfig.testRunner = 'jest-jasmine2'; | ||
| jestConfig.testRunner = getDefaultJestRunner(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| type Jest26CacheKeyOptions = { instrument: boolean; rootDir: string }; | ||
| type Jest26Config = { instrument: boolean; rootDir: string }; | ||
| type Jest27TransformOptions = { config: Jest26Config }; | ||
| export type JestPreprocessor = { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
alicewriteswrongs
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
tanner-reits
left a comment
There was a problem hiding this comment.
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
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)
4327cfc to
8dc8895
Compare
|
ditto on the upgrade guide - ideally just a README.md in the right spot so you can't miss it |
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:
this commit introduces the following infrastructue to dispatch to jest-27-and-under at runtime:
Where:
jest-stencil-connector- performs the detection of the current version of jest - dispatching to the correct infrastructure viajest-facadeimplementationsjest-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 injest-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?
Testing
Pull down this branch and build it. Make note of the location of the generated tarball:
Next, create a new stencil component library with that tarball:
Stencil Testing should continue to pass for Jest [24,27]:
Next, Stencil should continue to warn if a testing module is not installed:
Other information