timer still in the queue despite flush in fakeAsync - karma-jasmine

I have this test that will result in the infamous "1 timer(s) still in the queue" error:
import {
discardPeriodicTasks,
fakeAsync,
flush,
flushMicrotasks,
tick
} from "#angular/core/testing";
describe("Sleep", () => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
it("should sleep async", async () => {
let slept = false;
await sleep(0).then(() => (slept = true));
expect(slept).toBeTruthy();
});
it("should sleep fakeAsync", fakeAsync(async () => {
let slept = false;
await sleep(0).then(() => (slept = true));
flush();
flushMicrotasks();
discardPeriodicTasks();
tick(1000);
expect(slept).toBeTruthy();
}));
});
No amount of flushing or ticking including the hints from this answer will get rid of the timer. What else can I do? The variant without fakeAsync() works fine.
Stackblitz: https://stackblitz.com/edit/test-jasmine-karma-fakeasync-timer?file=app/test.ts

For whatever reason, it works if you convert the sleep(0) Promise into an Observable instead.
it("should sleep fakeAsync", fakeAsync(async () => {
let slept = false;
//await sleep(0).then(() => (slept = true));
from(sleep(0)).subscribe(() => (slept = true));
expect(slept).toBeFalsy();
tick(0);
expect(slept).toBeTruthy();
}));
I ran into a similar problem with debounceTime from Rxjs where no amount of flush(), flushMicroTasks() or discardPeriodicTasks() would release the debounce. However in my case I was able to resolve my problem by making a call to tick() with a sufficiently large time value after my expectation had finished to allow the debounceTime to complete.

I was able to solve the problem in stackblitz removing the await in fakeAsync, because the point of fakeAsync is run synchronously.
So, the modified working test is:
it("should sleep fakeAsync", fakeAsync(() => {
let slept = false;
sleep(100).then(() => (slept = true));
flush();
expect(slept).toBeTruthy();
}));
You just need to use flush to process your timeout time synchronously and the test will pass as expected. Another answer to support my point about fakeAsync and async: Angular testing: using fakeAsync with async/await.
I was still getting this error in my test for nested timeout, a timeout inside a service that is inside a request subscribe. This solution doesn't solve my problem. So I dropped the fakeAsync approach and use the one suggested here: Test a function that contains a setTimeout() and finally I solved my problem.

Related

Vitest LitElement events

I have a storybook project where i am using Vite and LitElement components.
To test the components i thought i would use the Vitest library.
But i can't really test my components, it is like if the components aren't initialized / mounted / working (but they work fine in the stories., so i think the problem is with the testing).
I have a breadcrumb component, which dispatches a custom event on the connectedCallback function. On my story i can listen to this event, so i know it is being dispatched.
But i can seem to test it.
What i have:
on the breadcrumb component, inside the connectedCallback function
this.dispatchEvent(new Event('abc-breadcrumb-connected'));
on my breadcrumb.test.ts file:
import type { IWindow } from 'happy-dom';
import { expect, describe, it, beforeEach, vi } from 'vitest';
import '../abc-breadcrumb';
import { AbcBreadcrumb } from "../abc-breadcrumb";
declare global {
interface Window extends IWindow {}
}
describe('Abc breadcrumb', async () => {
it('Dispatches connected event', async () => {
const mockConnectedCallback = vi.fn(() => true);
window.addEventListener('abc-breadcrumb-connected', () => {
console.log('GOT THE EVENT');
mockConnectedCallback()
});
document.body.innerHTML = `
<abc-breadcrumb role="nav" aria-label="Breadcrumb" class="breadcrumb" ismobile="">
...
</abc-breadcrumb>
`;
await window.happyDOM.whenAsyncComplete();
await new Promise(resolve => setTimeout(resolve, 0));
expect(mockConnectedCallback).toHaveBeenCalled();
})
});
0n my vite.config.ts i have:
export default defineConfig({
test: {
globals: true,
environment: 'happy-dom',
},
...
})
the error i get:
AssertionError: expected "spy" to be called at least once
I have no idea why it isn't working an would be really happy to get some help.
Thanks!
At first look, using await new Promise(resolve => setTimeout(resolve, 0)); seems like the kind of thing I was unsuccessfully relying on myself to ensure enough ticks had passed for various operations to complete. This often worked in my local browser but failed in CI or resulted in flaky tests.
Why not set up the promise such that the event listener calls resolve() or possibly even mockConnectedCallback(). Then you can be certain the event isn't firing as opposed to only not having been fired when setTimeout resolves.
const mockConnectedCallback = vi.fn(() => true);
let connectedResolve;
const connectedPromise = new Promise(r => connectedResolve = r);
window.addEventListener('abc-breadcrumb-connected', () => {
console.log('GOT THE EVENT');
connectedResolve();
mockConnectedCallback();
});
document.body.innerHTML = `
<abc-breadcrumb role="nav" aria-label="Breadcrumb" class="breadcrumb" ismobile="">
...
</abc-breadcrumb>
`;
await window.happyDOM.whenAsyncComplete();
await connectedPromise;
expect(mockConnectedCallback).toHaveBeenCalled();
It makes the expect() a little redundant given that it won't be reached until the awaited promise resolves, but I think awaiting an explicit resolution makes things easier to reason about and doesn't hard-code various assumptions about microtask queues and things into the test code, which more often than not have come back to bite me later.

Cypress E2E: Before hook not working on retries

Our tests have a reset state cypress command on the before hook that clears local storage and cookies so that before each run this function makes sure no session was stored.
But we are somehow getting test fail upon retry because it looks like its ignoring the reset state function and doesn't start the test on the signup page.
Is there a way to force the before hook when a retry happens?
The before-hook is excluded from retry as described here: https://docs.cypress.io/guides/guides/test-retries#How-It-Works
Also notable, if something went wrong within before-hook there will be no retry.
There is also an issue that requests this feature: https://github.com/cypress-io/cypress/issues/19458
As a workaround you can use a beforeEach:
let isError = false;
beforeEach(() => {
cy.once('fail', (err) => {
isError = true;
throw err;
});
if (isError) {
cy.resetAll(); // or whatever you have to do before retry
isError = false;
}
});
Explanation: If there is an assertion error then the 'fail'-event is triggered an is catched. In the catch-block the 'isError'-flag is set. Then on the first retry we make our reset-work and also resets the 'isError'-flag.
If you have some initial work that you do in before-hook normally, you have to do a little modification:
let isError = false;
let firstTime = true;
beforeEach(() => {
cy.once('fail', (err) => {
isError = true;
throw err;
});
if (isError) {
cy.resetAll();
}
if (firstTime || isError) {
firstTime = false;
isError = false;
// setupMyBackend()
}
});
Slightly simpler, make the before() callback callable elsewhere (declare and name it).
Use test:after:run event to call it depending on test results.
const beforeCallback = () => {...}
before(beforeCallback)
Cypress.on('test:after:run', (result) => {
if (result.currentRetry < result.retries && result.state === 'failed') {
beforeCallback()
}
})
it('fails', {retries:3}, () => expect(false).to.eq(true)) // failing test to check it out

nextTick() not triggering DOM update

I'm creating a messaging app and I'm having some trouble with scrolling to the bottom of an ion-content element when a new message is added to an array. I'm using the scrollToBottom() method that comes with ion-content, and I'm using the Composition API in Vue 3.
Consider this snippet:
setup(props) {
const replyContent = ref("")
const messages = ref([])
// References to ion-content in the template
const ionContent = ref(null)
const reply = async () => {
const message = await replyToThread(props.threadId, replyContent.value).then((message) => message)
messages.value.push(message)
nextTick(() => {
console.log("DOM updated!")
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
})
}
return { replyContent, messages, ionContent, reply }
}
replyToThread() performs an API call and returns the new message, and nextTick() should ensure me that the DOM has been updated so that I can have my way with it. The console does successfully log "DOM updated!", but no scrolling to the bottom happens.
But, and somehow this works every time nextTick() doesn't, when I replace the nextTick() code block with the following, it works flawlessly:
setTimeout(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
}, 200)
I have to set the timeout at around 200 ms, otherwise it doesn't work. But relying on this when something fancy like nextTick() should do the trick feels quite dirty. Does anyone know why this is happening?
That is because nextTick() only guarantees that the actual DOM has been updated: it doesn't mean that the browser has actually finished the layout of the page. That is the reason why you need an arbitrary timeout to ensure the scrolling works, because after 200ms the browser is likely to be done laying things out based on the updated DOM.
To fix this you will probably need to rely on window.requestAnimationFrame:
nextTick(() => {
window.requestAnimationFrame(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
});
});
If this feels like too much nesting for you, you can create a method that returns a promise based on rAF:
methods: {
rAF: function() {
return new Promise(r => window.requestAnimationFrame(r));
}
}
Then it's a matter of ensuring promises returned by both nextTick() and rAF() are resolved before scrolling:
await nextTick();
await this.rAF();
if (ionContent.value) {
ionContent.value.$el.scrollToBottom();
}

Jasmine Spy called incorrectly

I'm running into an interesting problem. I have set up a jasmine spy on an event listener that I attach and detach during the lifecycle of the component (this is in lit element by the way). On the connected callback I attach it like this:
getPositionEvent = this.getPosition.bind(this);
connectedCallback() {
super.connectedCallback();
window.addEventListener('resize', this.getPositionEvent, true);
}
I later detach it like this:
disconnectedCallback() {
window.removeEventListener('resize', this.getPositionEvent, true);
}
I can see during testing of the code that the attachment works here:
let getPositionEventSpy: jasmine.Spy;
beforeAll(() => {
(code setting up component)
getPositionEventSpy = spyOn(component, 'getPositionEvent');
}
beforeEach(async () => {
component.connectedCallback();
await component.updateComplete;
getPositionEventSpy.calls.reset();
})
it('should include an event listener for "resize"', async () => {
window.dispatchEvent(new Event('resize'));
await component.updateComplete;
expect(getPositionEventSpy.calls.count()).toEqual(1);
})
The problem comes when I try to test that the event listener is detached. I put this test in a separate describe block where I initiate a the disconnectedCallback function which should remove the event listener and then test that the spy has not been called when I dispatch the event:
describe('disconnection', () => {
beforeAll(() => {
component.disconnectedCallback();
getPositionEventSpy.calls.reset();
})
it('should remove the "resize" event listener', async () => {
expect(getPositionEventSpy.calls.count()).toEqual(0);
window.dispatchEvent(new Event('resize'));
await component.updateComplete;
expect(getPositionEventSpy).not.toHaveBeenCalled();
})
})
In this case the test fails meaning the getPositionEventSpy has been called. In trying to understand what's happening I added a console.log("getting position") statement in the getPosition() function. When I run the test for removing the event listener the console log statement doesn't get run, so I believe that the removal of the eventlistener is actually successful. So why does the spy count increase? Does anybody know?
try spy.resetHistory()
beforeAll(() => {
component.disconnectedCallback();
getPositionEventSpy.resetHistory();
})

How can I `await` closing of window in Spectron?

I'm working on Electron application that handles some logic in modal windows.
Those windows are waiting for async actions to resolve and then self-close. Now I am struggling to test this behaviour using Spectron and Jest: seems that there are no methods to catch window's closing and then proceed to another tests.
Currently my code is
it('doing its job', async () => {
// awaits and expects that aren't related
await app.client.click('button[data-role="close"]');
await new Promise(r => setTimeout(r, 1000));
expect(await client.getWindowCount()).toBe(1);
});
It works but I find it extremely anti-pattern. I wonder if there are any methods to do something like
it('doing its job', async () => {
// awaits and expects that aren't related
await app.client.click('button[data-role="close"]');
await app.client.waitUntilWindowCloses(windowIndex);
expect(await client.getWindowCount()).toBe(1);
});
Any help is appreciated.
await app.client.waitUntil(async () => (await app.client.getWindowCount()) === 1);