or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

code-organization-rules.mdcode-style-rules.mdexpectation-rules.mdindex.mdjasmine-matcher-rules.mdpromise-rules.mdspy-rules.mdtest-structure-rules.md
tile.json

promise-rules.mddocs/

Promise Handling Rules

Rules that enforce proper handling of promises and asynchronous code in Jasmine tests to prevent test flakiness and ensure reliable async testing.

Capabilities

no-promise-without-done-fail

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');
});

prefer-promise-strategies

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');
});

Rule Configuration

Both promise handling rules support standard ESLint severity levels:

  • 0 or "off" - Disable the rule
  • 1 or "warn" - Enable as warning
  • 2 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 patterns

Modern Async Testing Patterns

Using async/await

describe('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();
    });
  });
});

Using expectAsync for Promise Testing

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()
    ]);
  });
});

Legacy Pattern with Proper Error Handling

// 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
  });
});

Complex Async Scenarios

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);
    });
  });
});