TypeScript

Type-check your tests, register config names and write imports that Retest can load.

Retest runs TypeScript test files without a build step, but running a test does not check its types. Set up a separate type check to catch misspelt app names, wrong matcher arguments and methods your target cannot use.

Run the type check Link to Run the type check

retest init writes tests/tsconfig.json and adds a typecheck:e2e script to package.json. With TypeScript and Node's type definitions installed as in the Quick start, run that script before the tests.

npm run typecheck:e2e

The script runs tsc -p tests/tsconfig.json. If your project already has a root tsconfig, the generated file extends it while keeping the tests' compiler options and include list separate. It checks your test files and retest.config.ts without writing JavaScript.

Register your config Link to Register your config

Register the config's type once so every test in the same TypeScript program knows your names. Keep the declare module block that init writes, or add one after exporting your config.

retest.config.ts
import { chrome, defineConfig, env } from '@rehearsal-labs/retest'const config = defineConfig({  apps: {    web: chrome({ baseUrl: 'http://127.0.0.1:4173' }),  },  tags: ['smoke'],  secrets: { password: env('TASK_APP_PASSWORD') },  testIds: ['task-title', 'save-task', 'saved-task'],})export default configdeclare module '@rehearsal-labs/retest' {  interface Register {    config: typeof config  }}

defineConfig preserves the literal names in this object. Registration makes apps: ['web'], the smoke tag, the password secret and these three test ids available to your tests. A typo in one of those names becomes a type error.

This config also declares a password source. Set TASK_APP_PASSWORD to at least four characters before running it; a shorter value stops the run with exit code 2. Leave out secrets if the suite needs no password. The type check does not read secret values; Signing in and secrets explains how to use them.

tests/task.retest.ts
import { expect, test } from '@rehearsal-labs/retest'test('saves a task', { apps: ['web'], tags: ['smoke'] }, async ({ web }) => {  await web.goto('/')  await web.getByTestId('task-title').fill('Release checklist')  await web.getByTestId('save-task').click()  await expect(web.getByTestId('saved-task')).toHaveText('Release checklist')})

Because this test declares web, its callback receives web rather than page. It cannot use another app it did not declare. A test that omits apps uses the default app through page, as in Writing tests.

Without registration, tests can use page, but declaring apps or state produces an error explaining how to register. Registration applies to the whole TypeScript program, so give separate Retest projects separate tsconfigs rather than combining conflicting registrations.

Decide which names to restrict Link to Decide which names to restrict

You do not have to list every test id or tag to start writing tests. In a registered config, omitting testIds, tags or states leaves those names open to strings; listing them restricts the accepted names.

Declare secrets, inboxes and locks before using their names in a registered project. testIds restricts only the type check; your app must still provide those attributes. Retest checks the other declared resources when it loads or runs the tests.

If you keep test ids in a separate object, preserve their literal types with as const before passing the object as testIds. Otherwise its values can widen to string, which loses the restriction.

Follow the target capabilities Link to Follow the target capabilities

The page type follows the targets configured for its app. A web locator offers tap only when every target of that app emulates touch; click works for a web app with mixed mouse and touch targets.

An iOS app uses tap, a macOS app uses click, and neither has web navigation through goto. An Electron page is its app's first window and also has no goto. When a method does not apply, the type error tells you what to use instead.

Write module imports Link to Write module imports

Write test files and the config as ES modules using import and export. Without a "type" field in package.json, Node loads them but warns that it had to infer the module format. Setting "type": "module" avoids that warning; "type": "commonjs" prevents retest.config.ts from loading.

Changing a package to "type": "module" also changes how Node reads its .js files, which breaks existing CommonJS .js files. Keep those files in a CommonJS package or give them a .cjs extension. Import types with import type so Node does not try to load them as runtime values.

tests/support/task.ts
export type Task = { title: string }export function releaseTask(): Task {  return { title: 'Release checklist' }}
tests/task.retest.ts
import type { Task } from './support/task.ts'import { expect, test } from '@rehearsal-labs/retest'import { releaseTask } from './support/task.ts'test('saves a task', async ({ page }) => {  const task: Task = releaseTask()  await page.goto('/')  await page.getByTestId('task-title').fill(task.title)  await page.getByTestId('save-task').click()  await expect(page.getByTestId('saved-task')).toHaveText(task.title)})

The helper returns ordinary data, while import type keeps its type out of the runtime imports. Relative imports work with or without extensions. An import written as ./support/task.js also loads task.ts when that TypeScript source exists.

Use path aliases Link to Use path aliases

Retest reads paths from the nearest tsconfig.json at or above the importing file. Add an alias to the tests' tsconfig when a larger suite needs shared helpers in several folders.

tests/tsconfig.json
{  "compilerOptions": {    "baseUrl": ".",    "paths": {      "@support/*": ["./support/*"]    }  }}

Merge these options into the generated file, keeping its other settings and include list. The explicit baseUrl makes @support/task resolve to tests/support/task.ts. Retest follows extends, including an inherited baseUrl; without the local setting, a root "baseUrl": "." would send this alias to support/task.ts at the project root. With no baseUrl, paths resolve from the tsconfig that declares them.

The config loads with the tsconfig nearest its own folder, so retest.config.ts cannot use an alias declared only in tests/tsconfig.json. The generated type check includes the config under the tests' compiler settings, so that import can pass the type check and still fail to load. Put shared aliases in the root tsconfig or use relative imports in the config.

Retest does not follow TypeScript project references or resolve bare imports from baseUrl alone.

Know what can load Link to Know what can load

Node's TypeScript transformer handles types, enums, namespaces with values and parameter properties, while failures still point at your TypeScript lines. When importing app code into a test, check the restrictions below because its build tools may accept syntax Retest cannot load.

  • JSX and .tsx or .jsx files do not load. Keep test helpers separate from components that require JSX.
  • A decorator prevents its file and tests importing it from loading, so collection fails even when the tsconfig enables decorators.
  • CommonJS test files, configs and TypeScript, including .cts, are refused. A CommonJS JavaScript helper such as .cjs can be imported using Node's normal rules.
  • A type imported as a value can fail to load. Use import type for types and interfaces.

Retest reads import aliases from tsconfig, but it does not use settings such as target, jsx or experimentalDecorators to compile your code. A passing type check therefore complements a test run; it cannot guarantee that every imported file will load.

Next steps Link to Next steps