Testing embedded JavaScript in my headless browser (and a question) #1487
Replies: 4 comments
-
Hey there, cool project! Yes this pattern makes sense to me and mirrors stuff I've done in the past in similar situations. It's also how the And yes, you can use Assuming Here's a version that uses a few more of Gomega's affordances to make to guard against the test locking up on a chanel read that never happens: It("Defers execution", func() {
ctx := NewTestContext()
ctx.StartEventLoop()
c := make(chan any)
ctx.Window().
AddEventListener("go:home",
NewEventHandlerFunc(func(e Event) error {
close(c) // just close the channel to signify the event was received
return nil
}))
// Exercise - call setTimeout
Expect(
ctx.RunTestScript(`
let val; setTimeout(() => {
val = 42;
window.dispatchEvent(new CustomEvent("go:home"));
// throw new Error();
}, 1);
val`,
),
).To(BeNil())
Eventually(c).Should(BeClosed()) //wrap this in an eventually so your test doesn't block. This will poll the channel up to the default timeout (of 1 second)
Expect(ctx.RunTestScript(`val`)).To(BeEquivalentTo(42))
}) But my sense is you are only using the If so, I'd leverage the fact that Javascript runtimes are single-threaded and do this instead: It("Defers execution", func() {
ctx := NewTestContext()
ctx.StartEventLoop()
// Exercise - call setTimeout
Expect(
ctx.RunTestScript(`
let val; setTimeout(() => {
val = 42;
// throw new Error();
}, 1);
val`,
),
).To(BeNil())
Eventually(ctx.RunTestScript).WithArguments("val").Should(BeEquivalentTo(42))
}) This gets rid of the incidental stuff. Here Lastly, if you're interested - I built a thing called biloba to do headless browser testing in Ginkgo and Gomega. The project itself might be interesting (obviously, different goals than what you're building) but the test suite might be a good resource as it is in a similar domain as what you are working on. |
Beta Was this translation helpful? Give feedback.
-
Hey, thanks a lot for the feedback, greatly appreciated. I actually had a good feeling about the approach and I have already moved a lot to the
You are absolutely right, it's only for test synchronization. I generally prefer to avoid polling as much as possible, so the test will proceed as soon as it's ready. But the version you showed is certainly less code :) But eventually, I'll probably move that synchronisation to the event loop itself, with functions like But you did show some things I wasn't aware of, e.g. the |
Beta Was this translation helpful? Give feedback.
-
Makes sense all around. FWIW the polling is quite frequent and generally hasn't been a source of significant slow-down in my experience. But since you're building the event loop yourself, implementing In case you want to guard against tests that hang forever, here's a synchronous non-polling approach that uses Ginkgo's SpecContext to handle timeouts: It("Defers execution", SpecTimeout(time.Second), func(spCtx SpecContext) {
ctx := NewTestContext()
ctx.StartEventLoop()
c := make(chan any)
ctx.Window().
AddEventListener("go:home",
NewEventHandlerFunc(func(e Event) error {
close(c) // just close the channel to signify the event was received
return nil
}))
// Exercise - call setTimeout
Expect(
ctx.RunTestScript(`
let val; setTimeout(() => {
val = 42;
window.dispatchEvent(new CustomEvent("go:home"));
// throw new Error();
}, 1);
val`,
),
).To(BeNil())
select {
case <-c:
Expect(ctx.RunTestScript(`val`)).To(BeEquivalentTo(42))
case <-spCtx.Done():
return
}
}) (just sharing to point you at some of the other toys and tools at your disposal) |
Beta Was this translation helpful? Give feedback.
-
Thanks for that suggestion also. I didn't know about the |
Beta Was this translation helpful? Give feedback.
-
For my crazy idea, to write a headless browser, I need to have native Go objects exposed to JS code. Tests of JS code generally execute JS code and verify side effect in JS scope.
I now came to the point of implementing an event loop. It did surprise me that v8 doesn't include one (but now I understand why node.js and the browser have different return types for
setTimeout
and friends).This is the first test for
setTimeout
. Apart from verifying the immediatly observed behaviour; there's an implicit verification that no unhandled errors had occurred in JS scope. E.g., thethrow new Error()
in here causes the test to fail if added; although it has no effect on the actual verification on this particular test; as it's thrown after the observable side effect was created.The implicit verification occurs in an
AfterEach
node. Is this a good idea? Or is it an antipattern to do this? And could I place that verification in anDeferCleanup
block? The functionNewTestContext()
creates a v8 context, but also disposes that in aDeferCleanup
. It would make a lot of sense to have a variation of this that just implicitly checks for no unhandled errors.The test is still a bit verbose. As this is the first going down this path, I haven't yet extracted sensible helper functions. The unhandled error check is a perfect case of something that should just be wrapped in a helper.
Beta Was this translation helpful? Give feedback.
All reactions