oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
73.24k stars 2.69k forks source link

Support for React-testing-library #198

Closed gkiely closed 10 months ago

gkiely commented 2 years ago

Allow running tests with @testing-library/react.

Example repo: https://github.com/gkiely/bun-rtl

Steps:

bun install
bun wiptest

Error without environment: image

Error after enabling jsdom:

image
Jarred-Sumner commented 2 years ago

re: JSDOM, the next step here is to implement a vm polyfill using ShadowRealm

The vm polyfill can be done in JS.

Another option is to submit a PR to JSDOM that leverages ShadowRealm instead of the vm polyfill.

Electroid commented 1 year ago

We'll be tracking support for the vm module here: #401

gkiely commented 1 year ago

@Electroid Can we keep this open to confirm that react-testing-library works once the vm module work is completed? I am happy to do the testing.

EvHaus commented 1 year ago

Tested this with bun 0.5.8 and looks like the blocker isn't @testing-library itself, but rather we need support for some underlying DOM implementation. Running @testing-library tests directly gives you:

326 |   } = _temp === void 0 ? {} : _temp;
327 |
328 |   if (!baseElement) {
329 |     // default to document.body instead of documentElement to avoid output of potentially-large
330 |     // head elements (such as JSS style blocks) in debug output
331 |     baseElement = document.body;
                      ^
ReferenceError: Can't find variable: document
      at render (/project/node_modules/@testing-library/react/dist/@testing-library/react.esm.js:331:18)
      at /project/myfile.test.tsx:15:22

I think bun just needs a way to load some kind of DOM library like happy-dom or jsdom. At the moment they don't work.

Trying to import happy-dom fails with:

25 | Object.defineProperty(exports, "__esModule", { value: true });
26 | const NodeFetch = __importStar(require("node-fetch"));
27 | /**
28 |  * Fetch headers.
29 |  */
30 | class Headers extends NodeFetch.Headers {
   ^
TypeError: The superclass is not a constructor.
      at /project/node_modules/happy-dom/lib/fetch/Headers.js:30:0
      at /project/node_modules/happy-dom/lib/window/Window.js:108:18
      at /project/node_modules/happy-dom/lib/window/GlobalWindow.js:6:17
      at /project/node_modules/happy-dom/lib/index.js:8:23

Trying to import jsdom fails with:

error: Cannot find package "vm" from "/project/node_modules/jsdom/lib/api.js"
gkiely commented 1 year ago

I'll be testing this out tmrw unless @EvHaus beats me to it.

Jarred-Sumner commented 1 year ago

It uses kind of an insane hack where the context object's prototype becomes a special global object proxy (like window in browsers)

Lmk if that breaks stuff

gkiely commented 1 year ago

It works!

image
gkiely commented 1 year ago

I am seeing an issue using screen.getByText that I am investigating.

image
Jarred-Sumner commented 1 year ago

oh interesitng

EvHaus commented 1 year ago

@gkiely Can you share your code? I'm having a hard time figuring out how to get JSDom to be loaded in bun before any tests run.

Jarred-Sumner commented 1 year ago

@EvHaus can you try bun test --preload 'script-or-package-name'

gkiely commented 1 year ago

I'm using a variation on this, will update shortly: https://twitter.com/jarredsumner/status/1659391501871513607/photo/1

JSDom is not working for me, using happy-dom.

gkiely commented 1 year ago

Ok posted an update with a working version. Preload fixed the above error. https://github.com/gkiely/bun-rtl

bun test --preload './src/preload.js' App.test.jsx
Jarred-Sumner commented 1 year ago

I'll have a fix for the issue with node-fetch shortly. TLDR Is we polyfill node-fetch since in Bun it should just use the globalThis.fetch implementation, but our polyfill was only exporting fetch and not all the other things node-fetch exports.

Jarred-Sumner commented 1 year ago

@EvHaus I tried to get your commit running and there is definitely a bug with --preload. Will investigate that.

In the meantime, if you make helpers/happydom.ts look more like this:

import { GlobalRegistrator } from "@happy-dom/global-registrator";

GlobalRegistrator.register();

and add a bunfig.toml that looks like this:

[test]
preload = ["./helpers/happydom.ts"]

It gets a lot closer to working.

There are 218 test failures, but 20 pass and it console.log's a LOT of text

image
EvHaus commented 1 year ago

Nice! The main thing that was missing is a way for bun:test to have a global afterEach step because react-testing-library requires that you run their cleanup function after each test.

For now I manually added that to each test file and that got me a lot closer. All tests pass when run individually, but running them all causes bun to crash mid-way through with...

$ TZ=UTC bun test
bun test v0.6.2 (8d90d795)

StatusBadge.test.tsx:
✓ <StatusBadge /> > should render without failure [9.08ms]

Pagination.test.tsx:
✓ <Pagination /> > should render a button for each page [17.88ms]
✓ <Pagination /> > should render two ellipses if current page is far from both ends [5.68ms]
✓ <Pagination /> > should render an ellipsis if current page is far the end [4.57ms]
✓ <Pagination /> > should render an ellipsis if current page is far the start [2.86ms]
✓ <Pagination /> > should navigate to the page when clicking on that specific page [17.10ms]
error: script "test" exited with code 6 (SIGABRT)

...before all the tests have finshed.

You can try with latest commit in https://github.com/EvHaus/test-runner-benchmarks/commit/3d1305172843b5819fd990f8b1dfe9c4a57cde55

Jarred-Sumner commented 1 year ago

We will add support for registering hooks via bun:test in preload

Jarred-Sumner commented 1 year ago

Nice! The main thing that was missing is a way for bun:test to have a global afterEach step because react-testing-library requires that you run their cleanup function after each test.

For now I manually added that to each test file and that got me a lot closer. All tests pass when run individually, but running them all causes bun to crash mid-way through with...

$ TZ=UTC bun test
bun test v0.6.2 (8d90d795)

StatusBadge.test.tsx:
✓ <StatusBadge /> > should render without failure [9.08ms]

Pagination.test.tsx:
✓ <Pagination /> > should render a button for each page [17.88ms]
✓ <Pagination /> > should render two ellipses if current page is far from both ends [5.68ms]
✓ <Pagination /> > should render an ellipsis if current page is far the end [4.57ms]
✓ <Pagination /> > should render an ellipsis if current page is far the start [2.86ms]
✓ <Pagination /> > should navigate to the page when clicking on that specific page [17.10ms]
error: script "test" exited with code 6 (SIGABRT)

...before all the tests have finshed.

You can try with latest commit in EvHaus/test-runner-benchmarks@3d13051

I can repro the crash.

Call stack:

WTFCrashWithInfo(int, char const*, char const*, int) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/wtf/Assertions.h:758)
JSC::LocalAllocator::allocateSlowCase(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode) (@JSC::LocalAllocator::allocateSlowCase(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode):67)
JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::'lambda'()::operator()() const (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/LocalAllocatorInlines.h:41)
JSC::HeapCell* JSC::FreeList::allocateWithCellSize<JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::'lambda'()>(JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::'lambda'() const&, unsigned long) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/FreeListInlines.h:44)
JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/LocalAllocatorInlines.h:38)
JSC::GCClient::IsoSubspace::allocate(JSC::VM&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/IsoSubspaceInlines.h:34)
void* JSC::tryAllocateCellHelper<JSC::JSString, (JSC::AllocationFailureMode)0>(JSC::VM&, unsigned long, JSC::GCDeferralContext*) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/JSCellInlines.h:190)
void* JSC::allocateCell<JSC::JSString>(JSC::VM&, unsigned long) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/JSCellInlines.h:206)
JSC::JSString::create(JSC::VM&, WTF::Ref<WTF::StringImpl, WTF::RawPtrTraits<WTF::StringImpl>>&&) (/Users/jarred/Code/bun/node_modules/bun-webkit-macos-arm64/include/JavaScriptCore/JSString.h:187)
JSC::JSFunction::originalName(JSC::JSGlobalObject*) (@JSC::JSFunction::originalName(JSC::JSGlobalObject*):253)
JSC::JSBoundFunction::nameSlow(JSC::VM&) (@JSC::JSBoundFunction::nameSlow(JSC::VM&):47)
JSC::JSFunction::name(JSC::VM&) (@JSC::JSFunction::name(JSC::VM&):58)
JSC::getCalculatedDisplayName(JSC::VM&, JSC::JSObject*) (@JSC::getCalculatedDisplayName(JSC::VM&, JSC::JSObject*):65)
JSC::StackFrame::functionName(JSC::VM&) const (@JSC::StackFrame::functionName(JSC::VM&) const:43)
JSC::StackFrame::toString(JSC::VM&) const (@JSC::StackFrame::toString(JSC::VM&) const:13)
JSC::Interpreter::stackTraceAsString(JSC::VM&, WTF::Vector<JSC::StackFrame, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&) (@JSC::Interpreter::stackTraceAsString(JSC::VM&, WTF::Vector<JSC::StackFrame, 0ul, WTF::CrashOnOverflow, 16ul, WTF::FastMalloc> const&):35)
JSC::ErrorInstance::computeErrorInfo(JSC::VM&) (@JSC::ErrorInstance::computeErrorInfo(JSC::VM&):23)
JSC::Heap::finalizeUnconditionalFinalizers() (@JSC::Heap::finalizeUnconditionalFinalizers():306)
JSC::Heap::runEndPhase(JSC::GCConductor) (@JSC::Heap::runEndPhase(JSC::GCConductor):355)
JSC::Heap::runCurrentPhase(JSC::GCConductor, JSC::CurrentThreadState*) (@JSC::Heap::runCurrentPhase(JSC::GCConductor, JSC::CurrentThreadState*):97)
WTF::ScopedLambdaFunctor<void (JSC::CurrentThreadState&), JSC::Heap::collectInMutatorThread()::$_25>::implFunction(void*, JSC::CurrentThreadState&) (@WTF::ScopedLambdaFunctor<void (JSC::CurrentThreadState&), JSC::Heap::collectInMutatorThread()::$_25>::implFunction(void*, JSC::CurrentThreadState&):12)
JSC::callWithCurrentThreadState(WTF::ScopedLambda<void (JSC::CurrentThreadState&)> const&) (@JSC::callWithCurrentThreadState(WTF::ScopedLambda<void (JSC::CurrentThreadState&)> const&):45)
JSC::Heap::collectInMutatorThread() (@JSC::Heap::collectInMutatorThread():27)
JSC::Heap::waitForCollection(unsigned long long) (@JSC::Heap::waitForCollection(unsigned long long):42)
JSC::Heap::collectSync(JSC::GCRequest) (@JSC::Heap::collectSync(JSC::GCRequest):30)
::JSC__VM__runGC(JSC__VM *, bool) (/Users/jarred/Code/bun/src/bun.js/bindings/bindings.cpp:3453)
src.bun.js.bindings.shimmer.Shimmer("JSC","VM",src.bun.js.bindings.bindings.VM).cppFn (/Users/jarred/Code/bun/src/bun.js/bindings/shimmer.zig:186)
src.bun.js.bindings.bindings.VM.runGC (/Users/jarred/Code/bun/src/bun.js/bindings/bindings.zig:4683)
src.cli.test_command.TestCommand.run (/Users/jarred/Code/bun/src/cli/test_command.zig:720)
src.string_types.PathString.slice (/Users/jarred/Code/bun/src/string_types.zig:43)
src.cli.test_command.TestCommand.runAllTests.Context.begin (/Users/jarred/Code/bun/src/cli/test_command.zig:618)
src.bun.js.javascript.OpaqueWrap__anon_144322__struct_253123.callback (/Users/jarred/Code/bun/src/bun.js/javascript.zig:121)
::JSC__VM__holdAPILock(JSC__VM *, void *, void (*)(void *)) (/Users/jarred/Code/bun/src/bun.js/bindings/bindings.cpp:3492)
src.bun.js.bindings.shimmer.Shimmer("JSC","VM",src.bun.js.bindings.bindings.VM).cppFn (/Users/jarred/Code/bun/src/bun.js/bindings/shimmer.zig:186)
src.bun.js.bindings.bindings.VM.holdAPILock (/Users/jarred/Code/bun/src/bun.js/bindings/bindings.zig:4627)
src.bun.js.javascript.VirtualMachine.runWithAPILock (/Users/jarred/Code/bun/src/bun.js/javascript.zig:1710)
src.cli.test_command.TestCommand.runAllTests (/Users/jarred/Code/bun/src/cli/test_command.zig:632)
src.cli.test_command.TestCommand.exec (/Users/jarred/Code/bun/src/cli/test_command.zig:462)
src.cli.Command.start (/Users/jarred/Code/bun/src/cli.zig:1213)

We manually run the garbage collector after each file finishes executing and that seems to cause a crash due to a memory allocation failure happening inside the finalizer when getting function names from Error objects being finalized. I'm not sure yet if this is a bug in bun or in JSC (probably Bun)

Jarred-Sumner commented 1 year ago

After working around the JSC thing

The next crash happens when trying to diff a 2.7 GB string compared to null. It occurs in the test following this one:

> should render the children for the element

image
Jarred-Sumner commented 1 year ago

Current status:

react-testing-library should work now, minus the above issue with the test simulation.

EvHaus commented 1 year ago

Confirmed. Working well for me in bun 0.6.3. 👍

I was able to add it to my test runner benchmarks. If you're curious, here are the results:

'yarn workspace bun test' ran
    1.16 ± 0.02 times faster than 'yarn workspace vitest test --isolate=false'
    1.32 ± 0.04 times faster than 'yarn workspace jasmine test'
    4.46 ± 0.06 times faster than 'yarn workspace jest test'
    9.08 ± 0.13 times faster than 'yarn workspace vitest test'

Looks like bun is the new performance winner of my benchmark. 🎉

Jarred-Sumner commented 1 year ago

The very marginal improvement there over vitest, I think, is because of the timers. If you remove the waitFor code or (make it so it only waits one tick), what does it look like? When I checked last, waitFor was a hardcoded 500ms delay in each test that it was used.

Jarred-Sumner commented 1 year ago

ah there's only one usage of waitFor, interesting

EvHaus commented 1 year ago

If I remove the 1 usage of waitFor I get slightly better comparative results:

'yarn workspace bun test' ran
    1.45 ± 0.04 times faster than 'yarn workspace vitest test --isolate=false'
    1.98 ± 0.06 times faster than 'yarn workspace jasmine test'
    7.96 ± 0.22 times faster than 'yarn workspace jest test'
   17.32 ± 0.48 times faster than 'yarn workspace vitest test'
Jarred-Sumner commented 1 year ago

yeah i think our functions for printing the errors is pretty slow right now. There is a test failing in Bun's version due to lack of process.env.TZ support. Adding that shortly.

image
Jarred-Sumner commented 1 year ago

alright give it another try in the canary build

should be about 80% faster

image
Jarred-Sumner commented 1 year ago

the main remaining TODOs here are: 1) defer generating the Error message to when the getter is called. This is what Jest & Vitest already do, but we do not. We eagerly generate error messages which is super expensive (why await waitFor ... had such an impact) 2) run the transpiler in paralell. We are running it single-threaded. This is good for memory usage. But after a few files, it slows stuff down a bit

EvHaus commented 1 year ago

Confirmed. Latest canary is a bit better (results). Bun is now a clear winner 🎉

Jarred-Sumner commented 1 year ago

@EvHaus the waitFor thing still needs to be addressed - Bun needs to implement Jest fake timers, I guess. In both Vite & Bun about 5 seconds of the total is just the hardcoded waitFor delay. Vite is running multi-threaded so the delay is spread across the number of cores

gkiely commented 10 months ago

@Jarred-Sumner Feel free to close this.

donaldpipowitch commented 2 months ago

I try to switch from Jest+Testing Library to Bun+Testing Library and stumbled over this issue. It looks like I have to run afterEach(() => cleanup()); within each test file.

When I do it globally in a preload function like this:

import { cleanup } from '@testing-library/react';
import {
  getIsReactActEnvironment,
  setReactActEnvironment,
} from '@testing-library/react/dist/act-compat';
import { afterAll, afterEach, beforeAll } from 'bun:test';

let previousIsReactActEnvironment = getIsReactActEnvironment();
beforeAll(() => {
  previousIsReactActEnvironment = getIsReactActEnvironment();
  setReactActEnvironment(true);
});
afterEach(() => cleanup());
afterAll(() => {
  setReactActEnvironment(previousIsReactActEnvironment);
});

then I got For queries bound to document.body a global document has to be available.... Is someone running Bun with Testing Library without manually adding afterEach(() => cleanup()); in each test file?

hmidmrii commented 1 month ago

@donaldpipowitch, I had the same issue, I opened an issue on react testing library repo https://github.com/testing-library/react-testing-library/issues/1348

gkiely commented 1 month ago

@donaldpipowitch @hmidmrii You need to set up happy-dom before calling cleanup. https://github.com/oven-sh/bun/issues/198#issuecomment-1555565193

hmidmrii commented 1 month ago

@gkiely If you check the issue I opened on react testing library repo in my previous commit, you will find that I did that in the preload file, I even proposed two workarounds, but the casual way of using the exported screen will not work. here's the issue again: https://github.com/testing-library/react-testing-library/issues/1348

hmidmrii commented 1 month ago

I opened an issue on their repo because I did some logs on both preload cleanup (placing the cleanup code in the preload file) and local cleanup (placing the cleanup code in each test file) and saw that both act the same, so I expected it the issue to be from cleanup function from react testing library and not from afterEach exported by Bun.