Skip to content

Commit d08f9dc

Browse files
committed
feat: Add TypeScript test support with tsx loader (v4.0.0-beta.19)
- Implemented Option 1A: tsx/cjs loader for TypeScript test files in ESM - Added comprehensive TypeScript test support documentation - Created lib/utils/loaderCheck.js utility for TypeScript setup validation - Updated init command to include tsx and configure require: ['tsx/cjs'] - Added tsx as devDependency and optional peerDependency - Improved requireModules() to resolve packages from user's directory - Added helpful error messages when TypeScript tests found without loader - Created comprehensive test suite demonstrating TS features (enums, interfaces, imports) - All unit tests passing (488 passing) - TypeScript tests verified working with tsx/cjs loader Key Changes: * docs/typescript.md: New section on TypeScript tests in ESM with tsx/cjs * docs/configuration.md: Updated require section with ESM examples * lib/codecept.js: Added TypeScript validation and improved module resolution * lib/command/init.js: Auto-configure tsx/cjs for TypeScript projects * lib/utils/loaderCheck.js: NEW - TypeScript loader validation utility * package.json: Bumped version to 4.0.0-beta.19, added tsx * test/data/typescript-static-imports/: Complete TypeScript test examples Technical Details: - Uses tsx/cjs (CommonJS hooks) instead of tsx/esm (ESM loaders) - Works because Mocha internally uses require() for test loading - tsx/cjs intercepts require() calls and transpiles .ts files on-the-fly - Supports all TypeScript features: enums, interfaces, types, decorators - Zero configuration required (no tsconfig.json needed) - Fast transpilation using esbuild
1 parent 4ff9a23 commit d08f9dc

File tree

14 files changed

+1323
-23
lines changed

14 files changed

+1323
-23
lines changed

TYPESCRIPT_ESM_ARCHITECTURE.md

Lines changed: 793 additions & 0 deletions
Large diffs are not rendered by default.

docs/configuration.md

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,40 +33,78 @@ After running `codeceptjs init` it should be saved in test root.
3333

3434
## Require
3535

36-
Requires described module before run. This option is useful for assertion libraries, so you may `--require should` instead of manually invoking `require('should')` within each test file. It can be used with relative paths, e.g. `"require": ["/lib/somemodule"]`, and installed packages.
36+
Requires modules before running tests. This is useful for:
37+
- **Assertion libraries:** e.g., `'should'` instead of manually requiring it in each test
38+
- **TypeScript loaders:** Enable TypeScript test files in ESM or CommonJS projects
39+
- **Setup modules:** Initialize testing environment
40+
- **Custom modules:** With relative paths like `"require": ["./lib/setup"]`
3741

38-
You can register ts-node, so you can use Typescript in tests with ts-node package
42+
### TypeScript Support
3943

40-
```js
41-
exports.config = {
42-
tests: './*_test.js',
43-
timeout: 10000,
44-
output: '',
44+
#### For ESM Projects (CodeceptJS 4.x)
45+
46+
Use modern loaders that support ES Modules:
47+
48+
**Using tsx (recommended - fast, zero config):**
49+
50+
```typescript
51+
// codecept.conf.ts
52+
export const config = {
53+
tests: './**/*_test.ts',
54+
require: ['tsx/cjs'], // ← Modern TypeScript loader
4555
helpers: {},
4656
include: {},
47-
bootstrap: false,
48-
mocha: {},
49-
// require modules
50-
require: ['ts-node/register', 'should'],
5157
}
5258
```
5359

54-
For array of test pattern
60+
**Using ts-node/esm:**
5561

56-
```js
62+
```typescript
63+
// codecept.conf.ts
64+
export const config = {
65+
tests: './**/*_test.ts',
66+
require: ['ts-node/esm'], // ← Established TypeScript loader
67+
helpers: {},
68+
include: {},
69+
}
70+
```
71+
72+
> **Note:** For ts-node/esm, you need a tsconfig.json with ESM configuration. See [TypeScript documentation](/typescript) for details.
73+
74+
#### For CommonJS Projects (CodeceptJS 3.x)
75+
76+
Use the CommonJS loader:
77+
78+
```javascript
79+
// codecept.conf.js
5780
exports.config = {
58-
tests: ['./*_test.js', './sampleTest.js'],
59-
timeout: 10000,
60-
output: '',
81+
tests: './*_test.ts',
82+
require: ['ts-node/register'], // ← CommonJS TypeScript loader
6183
helpers: {},
6284
include: {},
63-
bootstrap: false,
64-
mocha: {},
65-
// require modules
66-
require: ['ts-node/register', 'should'],
6785
}
6886
```
6987

88+
### Multiple Requires
89+
90+
You can combine multiple modules:
91+
92+
```typescript
93+
// codecept.conf.ts
94+
export const config = {
95+
tests: ['./**/*_test.ts', './smoke_test.ts'],
96+
require: [
97+
'tsx/cjs', // TypeScript loader
98+
'should', // Assertion library
99+
'./lib/testSetup' // Custom setup
100+
],
101+
helpers: {},
102+
include: {},
103+
}
104+
```
105+
106+
Modules are loaded in the order specified, before any tests run.
107+
70108
## Dynamic Configuration
71109

72110
By default `codecept.json` is used for configuration. You can override its values in runtime by using `--override` or `-o` option in command line, passing valid JSON as a value:

docs/typescript.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,176 @@ Then a config file and new tests will be created in TypeScript format.
4343

4444
If a config file is set in TypeScript format (`codecept.conf.ts`) package `ts-node` will be used to run tests.
4545

46+
## TypeScript Tests in ESM (CodeceptJS 4.x) <Badge text="Since 4.0.0" type="tip"/>
47+
48+
CodeceptJS 4.x uses ES Modules (ESM) which requires a different approach for TypeScript test files. While TypeScript **config files** (`codecept.conf.ts`) are automatically transpiled, TypeScript **test files** need a loader.
49+
50+
### Using tsx (Recommended)
51+
52+
[tsx](https://tsx.is) is a modern, fast TypeScript loader built on esbuild. It's the recommended way to run TypeScript tests in CodeceptJS 4.x.
53+
54+
**Installation:**
55+
```bash
56+
npm install --save-dev tsx
57+
```
58+
59+
**Configuration:**
60+
```typescript
61+
// codecept.conf.ts
62+
export const config = {
63+
tests: './**/*_test.ts',
64+
require: ['tsx/cjs'], // ← Enable TypeScript loader for test files
65+
helpers: {
66+
Playwright: {
67+
url: 'http://localhost',
68+
browser: 'chromium'
69+
}
70+
}
71+
}
72+
```
73+
74+
That's it! Now you can write tests in TypeScript with full language support:
75+
76+
```typescript
77+
// login_test.ts
78+
Feature('Login')
79+
80+
Scenario('successful login', ({ I }) => {
81+
I.amOnPage('/login')
82+
I.fillField('email', '[email protected]')
83+
I.fillField('password', 'password123')
84+
I.click('Login')
85+
I.see('Welcome')
86+
})
87+
```
88+
89+
**Why tsx?**
90+
-**Fast:** Built on esbuild, much faster than ts-node
91+
- 🎯 **Zero config:** Works without tsconfig.json
92+
- 🚀 **Works with Mocha:** Uses CommonJS hooks that Mocha understands
93+
-**Complete:** Handles all TypeScript features (enums, decorators, etc.)
94+
95+
### Using ts-node/esm (Alternative)
96+
97+
If you prefer ts-node:
98+
99+
**Installation:**
100+
```bash
101+
npm install --save-dev ts-node
102+
```
103+
104+
**Configuration:**
105+
```typescript
106+
// codecept.conf.ts
107+
export const config = {
108+
tests: './**/*_test.ts',
109+
require: ['ts-node/esm'], // ← Use ts-node ESM loader
110+
helpers: { /* ... */ }
111+
}
112+
```
113+
114+
**Required tsconfig.json:**
115+
```json
116+
{
117+
"compilerOptions": {
118+
"module": "ESNext",
119+
"target": "ES2022",
120+
"moduleResolution": "node",
121+
"esModuleInterop": true
122+
},
123+
"ts-node": {
124+
"esm": true,
125+
"experimentalSpecifierResolution": "node"
126+
}
127+
}
128+
```
129+
130+
### Full TypeScript Features in Tests
131+
132+
With tsx or ts-node/esm, you can use complete TypeScript syntax including imports, enums, interfaces, and types:
133+
134+
```typescript
135+
// types.ts
136+
export enum Environment {
137+
TEST = 'test',
138+
STAGING = 'staging',
139+
PRODUCTION = 'production'
140+
}
141+
142+
export interface User {
143+
email: string
144+
password: string
145+
}
146+
147+
// login_test.ts
148+
import { Environment, User } from './types'
149+
150+
const testUser: User = {
151+
152+
password: 'password123'
153+
}
154+
155+
Feature('Login')
156+
157+
Scenario(`Login on ${Environment.TEST}`, ({ I }) => {
158+
I.amOnPage('/login')
159+
I.fillField('email', testUser.email)
160+
I.fillField('password', testUser.password)
161+
I.click('Login')
162+
I.see('Welcome')
163+
})
164+
```
165+
166+
### Troubleshooting TypeScript Tests
167+
168+
**Error: "Cannot find module" or "Unexpected token"**
169+
170+
This means the TypeScript loader isn't configured. Make sure:
171+
1. You have `tsx` or `ts-node` installed: `npm install --save-dev tsx`
172+
2. Your config includes the loader in `require` array: `require: ['tsx/cjs']`
173+
3. The loader is specified before test files are loaded
174+
175+
**Error: Module not found when importing from `.ts` files**
176+
177+
Make sure you're using a proper TypeScript loader (`tsx/cjs` or `ts-node/esm`).
178+
179+
**TypeScript config files vs test files**
180+
181+
Note the difference:
182+
- **Config files** (`codecept.conf.ts`, helpers): Automatically transpiled by CodeceptJS
183+
- **Test files** (`*_test.ts`): Need a loader specified in `config.require`
184+
185+
### Migration from CodeceptJS 3.x
186+
187+
If you're upgrading from CodeceptJS 3.x (CommonJS) to 4.x (ESM):
188+
189+
**Old setup (3.x):**
190+
```javascript
191+
// codecept.conf.js
192+
module.exports = {
193+
tests: './*_test.ts',
194+
require: ['ts-node/register'], // CommonJS loader
195+
helpers: {}
196+
}
197+
```
198+
199+
**New setup (4.x):**
200+
```typescript
201+
// codecept.conf.ts
202+
export const config = {
203+
tests: './*_test.ts',
204+
require: ['tsx/cjs'], // TypeScript loader
205+
helpers: {}
206+
}
207+
```
208+
209+
**Migration steps:**
210+
1. Install tsx: `npm install --save-dev tsx`
211+
2. Update package.json: `"type": "module"`
212+
3. Rename config to `codecept.conf.ts` and use `export const config = {}`
213+
4. Change `require: ['ts-node/register']` to `require: ['tsx/cjs']`
214+
5. Run tests: `npx codeceptjs run`
215+
46216
## Promise-Based Typings
47217

48218
By default, CodeceptJS tests are written in synchronous mode. This is a regular CodeceptJS test:

lib/codecept.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { globSync } from 'glob'
33
import shuffle from 'lodash.shuffle'
44
import fsPath from 'path'
55
import { resolve } from 'path'
6-
import { fileURLToPath } from 'url'
6+
import { fileURLToPath, pathToFileURL } from 'url'
77
import { dirname } from 'path'
8+
import { createRequire } from 'module'
89

910
const __filename = fileURLToPath(import.meta.url)
1011
const __dirname = dirname(__filename)
@@ -18,6 +19,7 @@ import ActorFactory from './actor.js'
1819
import output from './output.js'
1920
import { emptyFolder } from './utils.js'
2021
import { initCodeceptGlobals } from './globals.js'
22+
import { validateTypeScriptSetup } from './utils/loaderCheck.js'
2123
import recorder from './recorder.js'
2224

2325
import storeListener from './listener/store.js'
@@ -66,6 +68,21 @@ class Codecept {
6668
modulePath = `${modulePath}.js`
6769
}
6870
}
71+
} else {
72+
// For npm packages, resolve from the user's directory
73+
// This ensures packages like tsx are found in user's node_modules
74+
const userDir = global.codecept_dir || process.cwd()
75+
76+
try {
77+
// Use createRequire to resolve from user's directory
78+
const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
79+
const resolvedPath = userRequire.resolve(requiredModule)
80+
modulePath = pathToFileURL(resolvedPath).href
81+
} catch (resolveError) {
82+
// If resolution fails, try direct import (will check from CodeceptJS node_modules)
83+
// This is the fallback for globally installed packages
84+
modulePath = requiredModule
85+
}
6986
}
7087
// Use dynamic import for ESM
7188
await import(modulePath)
@@ -246,6 +263,13 @@ class Codecept {
246263
async run(test) {
247264
await container.started()
248265

266+
// Check TypeScript loader configuration before running tests
267+
const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
268+
if (tsValidation.hasError) {
269+
output.error(tsValidation.message)
270+
process.exit(1)
271+
}
272+
249273
// Ensure translations are loaded for Gherkin features
250274
try {
251275
const { loadTranslations } = await import('./mocha/gherkin.js')

lib/command/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export default async function (initPath) {
161161
isTypeScript = true
162162
extension = isTypeScript === true ? 'ts' : 'js'
163163
packages.push('typescript')
164-
packages.push('ts-node')
164+
packages.push('tsx') // Add tsx for TypeScript support
165165
packages.push('@types/node')
166166
}
167167

@@ -172,6 +172,7 @@ export default async function (initPath) {
172172
config.tests = result.tests
173173
if (isTypeScript) {
174174
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
175+
config.require = ['tsx/cjs'] // Add tsx/cjs loader for TypeScript tests
175176
}
176177

177178
// create a directory tests if it is included in tests path

0 commit comments

Comments
 (0)