| permalink | /typescript |
|---|---|
| title | TypeScript |
CodeceptJS supports type declaration for TypeScript. It means that you can write your tests in TS. Also, all of your custom steps can be written in TS
With the TypeScript writing CodeceptJS tests becomes much easier. If you configure TS properly in your project as well as your IDE, you will get the following features:
- Autocomplete (with IntelliSense) - a tool that streamlines your work by suggesting when you typing what function or property which exists in a class, what arguments can be passed to that method, what it returns, etc. Example:
- To show additional information for a step in a test. Example:
- Checks types - thanks to TypeScript support in CodeceptJS now allow to tests your tests. TypeScript can prevent some errors:
- invalid type of variables passed to function;
- calls no-exist method from PageObject or
Iobject; - incorrectly used CodeceptJS features;
CodeceptJS can initialize tests as a TypeScript project. When starting a new project with a standard installation via
npx codeceptjs init
Then select TypeScript as the first question:
? Do you plan to write tests in TypeScript? Yes
Then a config file and new tests will be created in TypeScript format.
If a config file is set in TypeScript format (codecept.conf.ts) package ts-node will be used to run tests.
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.
tsx is a modern, fast TypeScript loader built on esbuild. It's the recommended way to run TypeScript tests in CodeceptJS 4.x.
Installation:
npm install --save-dev tsxConfiguration:
// codecept.conf.ts
export const config = {
tests: './**/*_test.ts',
require: ['tsx/cjs'], // ← Enable TypeScript loader for test files
helpers: {
Playwright: {
url: 'http://localhost',
browser: 'chromium'
}
}
}That's it! Now you can write tests in TypeScript with full language support:
// login_test.ts
Feature('Login')
Scenario('successful login', ({ I }) => {
I.amOnPage('/login')
I.fillField('email', 'user@example.com')
I.fillField('password', 'password123')
I.click('Login')
I.see('Welcome')
})Why tsx?
- ⚡ Fast: Built on esbuild, much faster than ts-node
- 🎯 Zero config: Works without tsconfig.json
- 🚀 Works with Mocha: Uses CommonJS hooks that Mocha understands
- ✅ Complete: Handles all TypeScript features (enums, decorators, etc.)
⚠️ Note:ts-node/esmhas significant limitations with module resolution and doesn't work well with modern ESM TypeScript projects. We strongly recommend usingtsxinstead. The information below is provided for reference only.
ts-node/esm has several issues:
- Doesn't support
"type": "module"in package.json - Doesn't resolve extensionless imports or
.jsimports to.tsfiles - Requires explicit
.tsextensions in imports, which isn't standard TypeScript practice - Less reliable than
tsxfor ESM scenarios
If you still want to use ts-node/esm:
npm install --save-dev ts-node// codecept.conf.ts
export const config = {
tests: './**/*_test.ts',
require: ['ts-node/esm'],
helpers: { /* ... */ }
}// tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2022",
"moduleResolution": "node",
"esModuleInterop": true
},
"ts-node": {
"esm": true
}
}Critical Limitations:
- ❌ Cannot use
"type": "module"in package.json - ❌ Import statements must match the actual file (no automatic resolution)
- ❌ Module resolution doesn't work like standard TypeScript/Node.js ESM
Recommendation: Use tsx/cjs instead for a better experience.
With tsx or ts-node/esm, you can use complete TypeScript syntax including imports, enums, interfaces, and types:
// types.ts
export enum Environment {
TEST = 'test',
STAGING = 'staging',
PRODUCTION = 'production'
}
export interface User {
email: string
password: string
}
// login_test.ts
import { Environment, User } from './types'
const testUser: User = {
email: 'test@example.com',
password: 'password123'
}
Feature('Login')
Scenario(`Login on ${Environment.TEST}`, ({ I }) => {
I.amOnPage('/login')
I.fillField('email', testUser.email)
I.fillField('password', testUser.password)
I.click('Login')
I.see('Welcome')
})Error: "Cannot find module" or "Unexpected token"
This means the TypeScript loader isn't configured. Make sure:
- You have
tsxorts-nodeinstalled:npm install --save-dev tsx - Your config includes the loader in
requirearray:require: ['tsx/cjs'] - The loader is specified before test files are loaded
Error: Module not found when importing from .ts files
When using ts-node/esm with ESM, you need to use .js extensions in imports:
// This will cause an error in ESM mode:
import loginPage from "./pages/Login"
// Use .js extension instead:
import loginPage from "./pages/Login.js"TypeScript will resolve the .js import to your .ts file during compilation. This is the standard behavior for ESM + TypeScript.
Alternatively, use tsx/cjs which doesn't require explicit extensions.
TypeScript config files vs test files
Note the difference:
- Config files (
codecept.conf.ts, helpers): Automatically transpiled by CodeceptJS - Test files (
*_test.ts): Need a loader specified inconfig.require
If you're upgrading from CodeceptJS 3.x (CommonJS) to 4.x (ESM):
Old setup (3.x):
// codecept.conf.js
module.exports = {
tests: './*_test.ts',
require: ['ts-node/register'], // CommonJS loader
helpers: {}
}New setup (4.x):
// codecept.conf.ts
export const config = {
tests: './*_test.ts',
require: ['tsx/cjs'], // TypeScript loader
helpers: {}
}Migration steps:
- Install tsx:
npm install --save-dev tsx - Update package.json:
"type": "module" - Rename config to
codecept.conf.tsand useexport const config = {} - Change
require: ['ts-node/register']torequire: ['tsx/cjs'] - Run tests:
npx codeceptjs run
By default, CodeceptJS tests are written in synchronous mode. This is a regular CodeceptJS test:
I.amOnPage('/')
I.click('Login')
I.see('Hello!')Even thought we don't see any await, those commands are executed synchronously, one by one.
All methods of I object actually return promise and TypeScript linter requires to use await operator for those promises.
To trick TypeScript and allow writing tests in CodeceptJS manner we create typings where void is returned instead of promises. This way linter won't complain on async code without await, as no promise is returned.
Our philosophy here is: use await only when it is actually needed, don't add visual mess to your code prefixing each line with await. However, you might want to get a better control of your tests and follow TypeScript conventions.
This is why you might want to enable promise-based typings.
A previous test should be rewritten with awaits:
await I.amOnPage('/')
await I.click('Login')
await I.see('Hello!')Using await explicitly provides a beter control of execution flow. Some CodeceptJS users report that they increased stability of tests by adopting await for all CodeceptJS commands in their codebase.
If you select to use promise-based typings, type definitions will be generated so all actions to return a promise. Otherwise they will still return promises but it won't be relfected in type definitions.
To introduce promise-based typings into a current project edit codecept.conf.ts:
fullPromiseBased: true;and rebuild type definitions with
npx codeceptjs def
If you want to get types for your custom helper, you can add their automatically with CodeceptJS command npx codeceptjs def.
For example, if you add the new step printMessage for your custom helper like this:
// customHelper.ts
export class CustomHelper extends Helper {
printMessage(msg: string) {
console.log(msg)
}
}Then you need to add this helper to your codecept.conf.js like in this docs.
And then run the command npx codeceptjs def.
As result our steps.d.ts file will be updated like this:
/// <reference types='codeceptjs' />
type CustomHelper = import('./CustomHelper');
declare namespace CodeceptJS {
interface SupportObject { I: I }
interface Methods extends Puppeteer, CustomHelper {}
interface I extends WithTranslation<Methods> {}
namespace Translation {
interface Actions {}
}
}And now you can use autocomplete on your test.
Generation types for PageObject looks like for a custom helper, but steps.d.ts will look like:
/// <reference types='codeceptjs' />
type loginPage = typeof import('./loginPage');
type homePage = typeof import('./homePage');
type CustomHelper = import('./CustomHelper');
declare namespace CodeceptJS {
interface SupportObject { I: I, loginPage: loginPage, homePage: homePage }
interface Methods extends Puppeteer, CustomHelper {}
interface I extends WithTranslation<Methods> {}
namespace Translation {
interface Actions {}
}
}You can define custom strict locators that can be used in all methods taking a locator (parameter type LocatorOrString).
Example: A custom strict locator with a data property, which can be used like this:
I.click({ data: 'user-login' });In order to use the custom locator in TypeScript code, its type shape needs to be registered in the interface CustomLocators in your steps.d.ts file:
/// <reference types='codeceptjs' />
...
declare namespace CodeceptJS {
...
interface CustomLocators {
data: { data: string };
}
}The property keys used in the CustomLocators interface do not matter (only the types of the interface properties are used). For simplicity it is recommended to use the name that is also used in your custom locator itself.
You can also define more complicated custom locators with multiple (also optional) properties:
/// <reference types='codeceptjs' />
...
declare namespace CodeceptJS {
...
interface CustomLocators {
data: { data: string, value?: number, flag?: boolean };
}
}
