Rules that enforce proper handling of promises and asynchronous code in Jasmine tests to prevent test flakiness and ensure reliable async testing.
Prevents promises in tests without proper error handling through done.fail callbacks, ensuring async test failures are properly caught and reported.
/**
* Rule: no-promise-without-done-fail
* Ensures promises in tests have proper error handling with done.fail
* @config [0|1|2] - Rule severity level
*/
'jasmine/no-promise-without-done-fail': [0|1|2]Examples:
// ❌ Bad - promise without done.fail
it('should handle async operation', function(done) {
asyncOperation()
.then(function(result) {
expect(result).toBe('success');
done();
});
// Missing .catch(done.fail) - test may hang on rejection
});
// ❌ Bad - multiple promises without proper error handling
it('should handle multiple async operations', function(done) {
Promise.all([
asyncOperation1(),
asyncOperation2()
]).then(function(results) {
expect(results.length).toBe(2);
done();
});
// Missing error handling
});
// ✅ Good - promise with done.fail
it('should handle async operation', function(done) {
asyncOperation()
.then(function(result) {
expect(result).toBe('success');
})
.then(done)
.catch(done.fail);
});
// ✅ Good - async/await pattern (preferred for modern Jasmine)
it('should handle async operation', async function() {
const result = await asyncOperation();
expect(result).toBe('success');
});Encourages the use of modern promise handling strategies and Jasmine's async utilities over callback-based patterns.
/**
* Rule: prefer-promise-strategies
* Encourages modern async testing patterns over callback-based approaches
* @config [0|1|2] - Rule severity level
*/
'jasmine/prefer-promise-strategies': [0|1|2]Examples:
// ❌ Discouraged - callback-based async testing
it('should handle async operation', function(done) {
asyncOperation(function(error, result) {
if (error) {
done.fail(error);
return;
}
expect(result).toBe('success');
done();
});
});
// ✅ Better - promise-based with expectAsync
it('should handle async operation', function() {
const promise = asyncOperation();
return expectAsync(promise).toBeResolvedTo('success');
});
// ✅ Best - async/await pattern
it('should handle async operation', async function() {
const result = await asyncOperation();
expect(result).toBe('success');
});
// ✅ Good - expectAsync for promise state testing
it('should reject invalid input', function() {
const promise = processInvalidData();
return expectAsync(promise).toBeRejectedWith('Invalid input');
});Both promise handling rules support standard ESLint severity levels:
0 or "off" - Disable the rule1 or "warn" - Enable as warning2 or "error" - Enable as error (fails lint)Recommended Settings:
rules:
jasmine/no-promise-without-done-fail: 1 # Warning - prevents hanging tests
jasmine/prefer-promise-strategies: 1 # Warning - encourages modern patternsdescribe('UserService async operations', function() {
let userService, mockApiClient;
beforeEach(function() {
mockApiClient = jasmine.createSpyObj('apiClient', ['get', 'post', 'put', 'delete']);
userService = new UserService(mockApiClient);
});
describe('fetchUser', function() {
it('should fetch user by id', async function() {
const expectedUser = { id: 123, name: 'John Doe' };
mockApiClient.get.and.returnValue(Promise.resolve(expectedUser));
const result = await userService.fetchUser(123);
expect(mockApiClient.get).toHaveBeenCalledWith('/users/123');
expect(result).toEqual(expectedUser);
});
it('should handle fetch errors', async function() {
const error = new Error('User not found');
mockApiClient.get.and.returnValue(Promise.reject(error));
await expectAsync(userService.fetchUser(999)).toBeRejectedWith(error);
expect(mockApiClient.get).toHaveBeenCalledWith('/users/999');
});
});
describe('createUser', function() {
it('should create user successfully', async function() {
const userData = { name: 'Jane Smith', email: 'jane@example.com' };
const createdUser = { id: 456, ...userData };
mockApiClient.post.and.returnValue(Promise.resolve(createdUser));
const result = await userService.createUser(userData);
expect(mockApiClient.post).toHaveBeenCalledWith('/users', userData);
expect(result).toEqual(createdUser);
});
it('should validate user data before creating', async function() {
const invalidUserData = { name: '', email: 'invalid-email' };
await expectAsync(userService.createUser(invalidUserData))
.toBeRejectedWithError('Invalid user data');
expect(mockApiClient.post).not.toHaveBeenCalled();
});
});
});describe('Promise state testing', function() {
let asyncService;
beforeEach(function() {
asyncService = new AsyncService();
});
it('should resolve with correct data', function() {
const promise = asyncService.fetchData(123);
return expectAsync(promise).toBeResolvedTo({ id: 123, data: 'test' });
});
it('should reject with specific error', function() {
const promise = asyncService.fetchData(-1);
return expectAsync(promise).toBeRejectedWith('Invalid ID');
});
it('should handle timeout scenarios', function() {
const slowPromise = asyncService.slowOperation();
return expectAsync(slowPromise).toBePending();
});
// Multiple promise assertions
it('should handle multiple async operations', function() {
const promise1 = asyncService.operation1();
const promise2 = asyncService.operation2();
return Promise.all([
expectAsync(promise1).toBeResolved(),
expectAsync(promise2).toBeResolved()
]);
});
});// When you must use done callback pattern
describe('Legacy async patterns', function() {
it('should handle callback-based API', function(done) {
legacyAsyncFunction(function(error, result) {
if (error) {
done.fail(error);
return;
}
expect(result).toBeDefined();
expect(result.status).toBe('success');
done();
});
});
it('should handle promise with done pattern', function(done) {
promiseBasedFunction()
.then(function(result) {
expect(result).toBeTruthy();
return anotherPromiseFunction(result);
})
.then(function(finalResult) {
expect(finalResult).toBe('expected');
})
.then(done)
.catch(done.fail); // Critical for proper error handling
});
});describe('Complex async workflows', function() {
let workflowService, mockSteps;
beforeEach(function() {
mockSteps = {
validate: jasmine.createSpy('validate').and.returnValue(Promise.resolve(true)),
process: jasmine.createSpy('process').and.returnValue(Promise.resolve({ processed: true })),
save: jasmine.createSpy('save').and.returnValue(Promise.resolve({ id: 123 }))
};
workflowService = new WorkflowService(mockSteps);
});
it('should execute complete workflow', async function() {
const inputData = { name: 'test', value: 42 };
const result = await workflowService.executeWorkflow(inputData);
expect(mockSteps.validate).toHaveBeenCalledWith(inputData);
expect(mockSteps.process).toHaveBeenCalledWith(inputData);
expect(mockSteps.save).toHaveBeenCalledWith({ processed: true });
expect(result.success).toBe(true);
expect(result.id).toBe(123);
});
it('should handle workflow step failures', async function() {
mockSteps.process.and.returnValue(Promise.reject(new Error('Processing failed')));
const inputData = { name: 'test', value: 42 };
await expectAsync(workflowService.executeWorkflow(inputData))
.toBeRejectedWithError('Processing failed');
expect(mockSteps.validate).toHaveBeenCalled();
expect(mockSteps.process).toHaveBeenCalled();
expect(mockSteps.save).not.toHaveBeenCalled();
});
it('should handle parallel operations', async function() {
const operations = [
workflowService.operation1(),
workflowService.operation2(),
workflowService.operation3()
];
const results = await Promise.all(operations);
expect(results).toHaveSize(3);
results.forEach(result => {
expect(result.success).toBe(true);
});
});
});