feat(tstest): Enhance tstest with fluent API, suite grouping, tag filtering, fixture & snapshot testing, and parallel execution improvements

This commit is contained in:
2025-05-16 00:21:32 +00:00
parent 1c5cf46ba9
commit 2b01d949f2
30 changed files with 1504 additions and 173 deletions

3
test/debug.js Normal file
View File

@ -0,0 +1,3 @@
// Direct run to see TAP output
const { execSync } = require('child_process');
console.log(execSync('tsx test/tapbundle/test.debug.ts', { cwd: '/mnt/data/lossless/git.zone/tstest' }).toString());

View File

@ -45,4 +45,11 @@ const test6 = tap.skip.test('my 6th test -> should fail after 1000ms', async (to
await tools.delayFor(100);
});
await tap.start();
const testPromise = tap.start();
// Export promise for browser compatibility
if (typeof globalThis !== 'undefined') {
(globalThis as any).tapPromise = testPromise;
}
export default testPromise;

View File

@ -0,0 +1,19 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// Simple test to debug TAP output
tap.test('test 1', async () => {
console.log('Test 1 running');
expect(true).toBeTrue();
});
tap.test('test 2 - skip', async (toolsArg) => {
toolsArg.skip('Skipping test 2');
expect(false).toBeTrue();
});
tap.test('test 3', async () => {
console.log('Test 3 running');
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,101 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// Global state for testing lifecycle hooks
const lifecycleOrder: string[] = [];
tap.describe('Test Suite A', () => {
tap.beforeEach(async (toolsArg) => {
lifecycleOrder.push('Suite A - beforeEach');
});
tap.afterEach(async (toolsArg) => {
lifecycleOrder.push('Suite A - afterEach');
});
tap.test('test 1 in suite A', async (toolsArg) => {
lifecycleOrder.push('Test 1');
expect(true).toBeTrue();
});
tap.test('test 2 in suite A', async (toolsArg) => {
lifecycleOrder.push('Test 2');
expect(true).toBeTrue();
});
tap.describe('Nested Suite B', () => {
tap.beforeEach(async (toolsArg) => {
lifecycleOrder.push('Suite B - beforeEach');
});
tap.afterEach(async (toolsArg) => {
lifecycleOrder.push('Suite B - afterEach');
});
tap.test('test 1 in nested suite B', async (toolsArg) => {
lifecycleOrder.push('Nested Test 1');
expect(true).toBeTrue();
});
});
});
// Test outside any suite
tap.test('test outside suites', async (toolsArg) => {
lifecycleOrder.push('Outside Test');
expect(true).toBeTrue();
});
tap.describe('Test Suite with errors', () => {
tap.beforeEach(async (toolsArg) => {
// Setup that might fail
const data = await Promise.resolve({ value: 42 });
toolsArg.testData = data;
});
tap.test('test with error', async (toolsArg) => {
// Verify that data from beforeEach is available
expect(toolsArg.testData).toBeDefined();
expect(toolsArg.testData.value).toEqual(42);
// Test that error handling works by catching an error
try {
throw new Error('Intentional error');
} catch (error) {
expect(error.message).toEqual('Intentional error');
}
});
tap.test('test with skip in suite', async (toolsArg) => {
toolsArg.skip('Skipping this test in a suite');
expect(false).toBeTrue();
});
});
// Verify lifecycle order - this test runs last to check if all hooks were called properly
tap.test('verify lifecycle hook order', async (toolsArg) => {
// Wait a bit to ensure all tests have completed
await new Promise(resolve => setTimeout(resolve, 50));
console.log('Lifecycle order:', lifecycleOrder);
// Check that the tests we expect to have run actually did
expect(lifecycleOrder).toContain('Test 1');
expect(lifecycleOrder).toContain('Test 2');
expect(lifecycleOrder).toContain('Nested Test 1');
// Check that beforeEach was called before each test in Suite A
const test1Index = lifecycleOrder.indexOf('Test 1');
expect(test1Index).toBeGreaterThan(-1);
const beforeTest1 = lifecycleOrder.slice(0, test1Index);
expect(beforeTest1).toContain('Suite A - beforeEach');
// Check that afterEach was called after test 1
const afterTest1 = lifecycleOrder.slice(test1Index + 1);
expect(afterTest1).toContain('Suite A - afterEach');
// Check nested suite lifecycle
const nestedTest1Index = lifecycleOrder.indexOf('Nested Test 1');
expect(nestedTest1Index).toBeGreaterThan(-1);
const beforeNestedTest1 = lifecycleOrder.slice(0, nestedTest1Index);
expect(beforeNestedTest1).toContain('Suite B - beforeEach');
});
tap.start();

View File

@ -0,0 +1,120 @@
import { tap, TapTools } from '../../ts_tapbundle/index.js';
import { expect } from '@push.rocks/smartexpect';
// Define fixture factories
interface User {
id: number;
name: string;
email: string;
role: string;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
tags: string[];
}
// Define user fixture factory
TapTools.defineFixture<User>('user', (data) => {
const id = data?.id || Math.floor(Math.random() * 10000);
return {
id,
name: data?.name || `Test User ${id}`,
email: data?.email || `user${id}@test.com`,
role: data?.role || 'user'
};
});
// Define post fixture factory
TapTools.defineFixture<Post>('post', async (data) => {
const id = data?.id || Math.floor(Math.random() * 10000);
return {
id,
title: data?.title || `Post ${id}`,
content: data?.content || `Content for post ${id}`,
authorId: data?.authorId || 1,
tags: data?.tags || ['test', 'sample']
};
});
tap.describe('Fixture System', () => {
tap.afterEach(async () => {
// Clean up fixtures after each test
await TapTools.cleanupFixtures();
});
tap.tags('unit', 'fixtures')
.test('should create a simple fixture', async (toolsArg) => {
const user = await toolsArg.fixture<User>('user');
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
expect(user.role).toEqual('user');
});
tap.tags('unit', 'fixtures')
.test('should create fixture with custom data', async (toolsArg) => {
const admin = await toolsArg.fixture<User>('user', {
name: 'Admin User',
role: 'admin'
});
expect(admin.name).toEqual('Admin User');
expect(admin.role).toEqual('admin');
expect(admin.email).toContain('@test.com');
});
tap.tags('unit', 'fixtures')
.test('should create multiple fixtures with factory', async (toolsArg) => {
const userFactory = toolsArg.factory<User>('user');
const users = await userFactory.createMany(3);
// Try different approach
expect(users.length).toEqual(3);
expect(users[0].id).not.toEqual(users[1].id);
expect(users[0].email).not.toEqual(users[1].email);
});
tap.tags('unit', 'fixtures')
.test('should create fixtures with custom data per instance', async (toolsArg) => {
const postFactory = toolsArg.factory<Post>('post');
const posts = await postFactory.createMany(3, (index) => ({
title: `Post ${index + 1}`,
tags: [`tag${index + 1}`]
}));
expect(posts[0].title).toEqual('Post 1');
expect(posts[1].title).toEqual('Post 2');
expect(posts[2].title).toEqual('Post 3');
expect(posts[0].tags).toContain('tag1');
expect(posts[1].tags).toContain('tag2');
});
tap.tags('unit', 'fixtures')
.test('should handle related fixtures', async (toolsArg) => {
const user = await toolsArg.fixture<User>('user', { name: 'Author' });
const post = await toolsArg.fixture<Post>('post', {
title: 'My Article',
authorId: user.id
});
expect(post.authorId).toEqual(user.id);
});
tap.tags('unit', 'fixtures', 'error')
.test('should throw error for undefined fixture', async (toolsArg) => {
try {
await toolsArg.fixture('nonexistent');
expect(true).toBeFalse(); // Should not reach here
} catch (error: any) {
expect(error.message).toContain('Fixture \'nonexistent\' not found');
}
});
});
tap.start();

View File

@ -0,0 +1,32 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// Test with fluent syntax
tap.tags('unit', 'fluent')
.priority('high')
.test('test with fluent syntax', async (toolsArg) => {
expect(true).toBeTrue();
toolsArg.context.set('fluentTest', 'works');
});
// Chain multiple settings
tap.tags('integration')
.priority('low')
.retry(3)
.timeout(5000)
.test('test with multiple settings', async (toolsArg) => {
expect(true).toBeTrue();
});
// Test context access from fluent test
tap.tags('unit')
.test('verify fluent context', async (toolsArg) => {
const fluentValue = toolsArg.context.get('fluentTest');
expect(fluentValue).toEqual('works');
});
// Test without tags - should show all tests run without filtering
tap.test('regular test without tags', async (toolsArg) => {
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,52 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// Test basic snapshot functionality
tap.tags('unit', 'snapshot')
.test('should match string snapshot', async (toolsArg) => {
const testString = 'Hello, World!';
await toolsArg.matchSnapshot(testString);
});
// Test object snapshot
tap.tags('unit', 'snapshot')
.test('should match object snapshot', async (toolsArg) => {
const testObject = {
name: 'Test User',
age: 30,
hobbies: ['reading', 'coding', 'gaming'],
metadata: {
created: '2024-01-01',
updated: '2024-01-15'
}
};
await toolsArg.matchSnapshot(testObject);
});
// Test named snapshots
tap.tags('unit', 'snapshot')
.test('should handle multiple named snapshots', async (toolsArg) => {
const config1 = { version: '1.0.0', features: ['a', 'b'] };
const config2 = { version: '2.0.0', features: ['a', 'b', 'c'] };
await toolsArg.matchSnapshot(config1, 'config_v1');
await toolsArg.matchSnapshot(config2, 'config_v2');
});
// Test dynamic content with snapshot
tap.tags('unit', 'snapshot')
.test('should handle template snapshot', async (toolsArg) => {
const template = `
<div class="container">
<h1>Welcome</h1>
<p>This is a test template</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
`.trim();
await toolsArg.matchSnapshot(template, 'html_template');
});
tap.start();

View File

@ -0,0 +1,49 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// First test sets some data and has tags
tap.tags('unit', 'context')
.priority('high')
.test('test with tags and context setting', async (toolsArg) => {
// Set some data in context
toolsArg.context.set('testData', { value: 42 });
toolsArg.context.set('users', ['alice', 'bob']);
expect(true).toBeTrue();
});
// Second test reads the context data
tap.tags('unit', 'context')
.test('test reading context', async (toolsArg) => {
// Read data from context
const testData = toolsArg.context.get('testData');
const users = toolsArg.context.get('users');
expect(testData).toEqual({ value: 42 });
expect(users).toContain('alice');
expect(users).toContain('bob');
});
// Test without tags - should be skipped when filtering by tags
tap.test('test without tags', async (toolsArg) => {
expect(true).toBeTrue();
});
// Test with different tags
tap.tags('integration')
.priority('low')
.test('integration test', async (toolsArg) => {
expect(true).toBeTrue();
});
// Test context cleanup
tap.tags('unit')
.test('test context operations', async (toolsArg) => {
// Set and delete
toolsArg.context.set('temp', 'value');
expect(toolsArg.context.get('temp')).toEqual('value');
toolsArg.context.delete('temp');
expect(toolsArg.context.get('temp')).toBeUndefined();
});
tap.start();

View File

@ -0,0 +1,85 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// Test skip functionality
tap.test('should skip a test with skip()', async (toolsArg) => {
toolsArg.skip('This test is skipped');
// This code should not run
expect(false).toBeTrue();
});
tap.test('should conditionally skip with skipIf()', async (toolsArg) => {
const shouldSkip = true;
toolsArg.skipIf(shouldSkip, 'Condition met, skipping');
// This code should not run
expect(false).toBeTrue();
});
tap.test('should not skip when skipIf condition is false', async (toolsArg) => {
const shouldSkip = false;
toolsArg.skipIf(shouldSkip, 'Should not skip');
// This code should run
expect(true).toBeTrue();
});
// Test todo functionality
tap.test('should mark test as todo', async (toolsArg) => {
toolsArg.todo('Not implemented yet');
// Test code that would be implemented later
expect(true).toBeTrue();
});
// Test timeout functionality
tap.test('should set custom timeout', async (toolsArg) => {
toolsArg.timeout(5000);
// Simulate a task that takes 100ms
await toolsArg.delayFor(100);
expect(true).toBeTrue();
});
// This test is expected to fail due to timeout
tap.test('should timeout when exceeding limit', async (toolsArg) => {
toolsArg.timeout(100);
// This test will timeout and be marked as failed by the test runner
await toolsArg.delayFor(2000);
// This line should not be reached due to timeout
});
tap.test('timeout should work properly', async (toolsArg) => {
toolsArg.timeout(200);
// This test should complete successfully within the timeout
await toolsArg.delayFor(50);
expect(true).toBeTrue();
});
// Test retry functionality
tap.retry(3)
.test('should retry on failure', async (toolsArg) => {
// Use retry count to determine success
const currentRetry = toolsArg.retryCount;
// Fail on first two attempts (0 and 1), succeed on third (2)
if (currentRetry < 2) {
throw new Error(`Attempt ${currentRetry + 1} failed`);
}
expect(currentRetry).toEqual(2);
});
tap.test('should expose retry count', async (toolsArg) => {
toolsArg.retry(2);
// The retry count should be available
expect(toolsArg.retryCount).toBeLessThanOrEqual(2);
expect(true).toBeTrue();
});
// Test allowFailure
tap.test('should allow failure', async (toolsArg) => {
// Just verify that allowFailure() can be called without throwing
toolsArg.allowFailure();
expect(true).toBeTrue();
// Note: In a real implementation, we would see "please note: failure allowed!"
// in the output when this test fails, but the test itself will still be marked as failed
});
tap.start();

View File

@ -17,9 +17,9 @@ const test3 = tap.test(
async () => {
expect(
(await test1.testPromise).hrtMeasurement.milliSeconds <
(await test2).hrtMeasurement.milliSeconds,
(await test2.testPromise).hrtMeasurement.milliSeconds,
).toBeTrue();
expect((await test2.testPromise).hrtMeasurement.milliSeconds > 1000).toBeTrue();
expect((await test2.testPromise).hrtMeasurement.milliSeconds >= 1000).toBeTrue();
},
);
@ -46,4 +46,4 @@ const test7 = tap.test('my 7th test -> should print a colored string', async (to
console.log(cs);
});
tap.start();
tap.start();

View File

@ -0,0 +1,16 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
import * as fs from 'fs';
// Test to demonstrate parallel execution timing - run with glob pattern
// This will give us a clear view of execution order with timestamps
const timestamp = () => new Date().toISOString().substr(11, 12);
tap.test('demo test in main file', async (toolsArg) => {
console.log(`[${timestamp()}] Test parallel demo started`);
await toolsArg.delayFor(1000);
console.log(`[${timestamp()}] Test parallel demo completed`);
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,11 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// This test runs in parallel group 2
tap.test('api test in parallel group 2', async (toolsArg) => {
console.log('API test started');
await toolsArg.delayFor(800);
console.log('API test completed');
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,13 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// This test runs in parallel group 1
const timestamp = () => new Date().toISOString().substr(11, 12);
tap.test('auth test in parallel group 1', async (toolsArg) => {
console.log(`[${timestamp()}] Auth test started`);
await toolsArg.delayFor(1000);
console.log(`[${timestamp()}] Auth test completed`);
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,11 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// This test runs in parallel group 2
tap.test('db test in parallel group 2', async (toolsArg) => {
console.log('DB test started');
await toolsArg.delayFor(800);
console.log('DB test completed');
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,10 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// This test runs serially (no para__ in filename)
tap.test('serial test 1', async (toolsArg) => {
await toolsArg.delayFor(500);
console.log('Serial test 1 completed');
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,10 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// This test runs serially (no para__ in filename)
tap.test('serial test 2', async (toolsArg) => {
await toolsArg.delayFor(500);
console.log('Serial test 2 completed');
expect(true).toBeTrue();
});
tap.start();

View File

@ -0,0 +1,13 @@
import { tap, expect } from '../../ts_tapbundle/index.js';
// This test runs in parallel group 1
const timestamp = () => new Date().toISOString().substr(11, 12);
tap.test('user test in parallel group 1', async (toolsArg) => {
console.log(`[${timestamp()}] User test started`);
await toolsArg.delayFor(1000);
console.log(`[${timestamp()}] User test completed`);
expect(true).toBeTrue();
});
tap.start();