Get started

Getting Started

Installation

Introduction

CSTesting is a Node.js testing framework that includes:

  • Test runnerdescribe / it, hooks, assertions (Jest-style)
  • Browser automation — CDP-based (Chrome/Chromium), no extra browser download
  • API testing — Rest-Assured style HTTP requests and status/body assertions
  • Config-driven tests — Run full flows from a simple config file (no code)

You can run unit tests, API tests, and browser tests in one runner, and use config files for quick flows or non-developers.

You will learn

  • How to install CSTesting
  • What gets created when you scaffold
  • How to run tests
  • How to open the HTML test report
  • How to update CSTesting
  • System requirements

Installing CSTesting

Using npm

Add CSTesting to a new or existing Node.js project:

npm install cstesting

No global install needed. Use the CLI via npx:

npx cstesting
npx cst

Scaffold (optional)

Scaffold a Page Object Model structure (pages + tests with sample code):

npx cstesting init
# or
npx cst init

The command does not overwrite existing files; it only creates missing folders and sample files. No prompts.

CSTesting uses the system Chrome or Chromium (via chrome-launcher). There is no separate browser download step.

What's installed

After npm install cstesting and (optionally) npx cstesting init:

  • package.json — Your project and dependencies (plus cstesting).
  • pages/ — Created by init. Page objects (e.g. HomePage.js) that wrap selectors and actions.
  • tests/ — Created by init. Sample test file home.test.js. You can add any *.test.js, *.spec.js, or (with ts-node) *.test.ts / *.spec.ts.

There is no single config file for the test run. Browser options are set per call in code (e.g. createBrowser({ headless: true })) or in config-driven .conf files.

Config-driven flows: You can create .conf files (e.g. login.conf) with steps like goto:url, click=selector, and run them with npx cstesting run login.conf — no test code required.

Writing tests

Create a test file (e.g. math.test.js):

const { describe, it, expect } = require('cstesting');

describe('Math', () => {
  it('adds numbers', () => {
    expect(1 + 1).toBe(2);
  });

  it('compares objects', () => {
    expect({ a: 1 }).toEqual({ a: 1 });
  });
});

Running tests

  • Default: Tests run headless (no visible browser window).
  • See the browser: In code use createBrowser({ headless: false }). In a .conf file set headed=true or headless=false.
npx cstesting
npx cstesting tests/
npx cstesting "**/*.test.js"
npx cstesting path/to/file.test.js

Test files run in sequence. Filter by path or pattern: pass a folder, a file path, or a glob.

HTML test report

After each run, CSTesting writes an HTML report to report/report.html (the report/ folder is created if missing). The report lists all tests with pass/fail/skip, duration, and expandable steps (if you use step('name') in tests). Failed tests show the error message and stack.

The report path is printed in the terminal after each run. Open it in your browser:

# Windows
start report\report.html

# macOS
open report/report.html

# Linux
xdg-open report/report.html

Updating CSTesting

Update to the latest version:

npm install cstesting@latest

No separate browser install; CSTesting uses the system Chrome/Chromium.

Check the installed version:

npm list cstesting

System requirements

  • Node.js: 16.0.0 or later (see engines in package.json).
  • OS: Any OS where Node.js and Chrome or Chromium run (Windows, macOS, Linux).
  • Browser: Chrome or Chromium must be installed on the machine. CSTesting launches it via chrome-launcher.
  • Mobile emulation: Not supported.

Page Object Model (POM)

After installing, scaffold a pages and tests structure with sample code:

npx cstesting init
# or
npx cst init

This creates:

  • pages/ — page objects (e.g. HomePage.js) that wrap selectors and actions
  • tests/ — sample test file (home.test.js) that uses the page object

Then run: npx cstesting tests/

Config-driven tests

Introduction

Config-driven tests let you run browser flows from a config file without writing test code. You define steps (navigate, type, click) in a simple text file; CSTesting runs them in the browser and produces a report. Each section in the config becomes one test case in the report.

This is useful when you want to run or share flows quickly, when non-developers need to define tests, or when you want to keep flows in a single file that can be edited without touching code.

You will learn

  • What config-driven tests are and when to use them
  • The config file format (syntax and options)
  • How to run a config file from the command line
  • How to run a config file from code
  • How the HTML report works for config runs
  • Locator syntax you can use in config

What are config-driven tests?

  • No test code — You write a .conf (or .config) file with one step per line.
  • Browser automation — CSTesting opens Chrome/Chromium, runs each step in order (goto, type into inputs, click), and stops on the first failure in a test case.
  • One test case per section — Lines starting with # start a new test case; all following steps belong to it until the next #. The report shows one row per test case with expandable steps.
  • Same report as code tests — After the run, an HTML report is written to report/report.html with pass/fail, duration, and step list.

Supported steps today: goto, type (into an input), and click. More actions (e.g. dropdowns, checkboxes, waits) may be added in future versions.

Config file format

Use one step per line. Empty lines and lines that don't match any step are ignored.

1. Test case name (section header)

A line that starts with # starts a new test case. The rest of the line is the test case name (used in the report).

# Login Page - Mercury Tours
# My first flow

All following steps (until the next #) belong to this test case. If the file has no # line, all steps form a single test case.

2. Browser mode (headless / headed)

  • headless=true — Run without a visible browser window (default).
  • headless=false — Run with a visible browser window.
  • headed=true — Same as headless=false (visible window).
  • headed=false — Same as headless=true.

These lines are options, not steps; they don't appear as steps in the report. The last value before a step applies.

headed=true
# Login flow
goto:https://example.com/login

3. Navigate to a URL

goto:https://example.com/login

The browser navigates to the URL. CSTesting waits for the page load (with a timeout) before continuing.

4. Type text into an element

Format: <label>:<locator>=value:<text>

  • label — Short name for the step (e.g. username, password). Shown in the report.
  • locator — How to find the element (see Locators in config).
  • text — The value to type. Everything after =value: is used as the text (so the text can contain spaces or colons).

Examples:

username:#email=value:user@test.com
password:#password=value:secret
name:[name="userName"]=value:mercury

The runner waits for the element to be present (with a timeout), then fills it (focus, set value, fire input/change events). Use this for text inputs and similar fields.

5. Click an element

Format: click=<locator>

Examples:

click=button[type="submit"]
click=[name="submit"]
click=#login-btn

The runner waits for the element, clicks it, then waits for the page load. Use this for submit buttons, links, or any clickable element.

Full example

login.conf

# Login Page (visible browser)
headed=true
goto:https://example.com/login
username:#email=value:user@test.com
password:#password=value:secret
click=button[type="submit"]

# Second test case - same file
# Simple visit
goto:https://example.com
click=a
  • First test case: Login Page (visible browser) — headed, goto login page, type username and password, click submit.
  • Second test case: Simple visit — goto example.com, click the first link.

Running a config file

From the command line

Option 1: Use the run subcommand and pass the config path.

npx cstesting run login.conf
npx cstesting run path/to/my.conf

Option 2: Pass the config file directly (only if the path has extension .conf or .config).

npx cstesting login.conf
npx cstesting flows/demo.config

The CLI looks for the file in the current directory (or the path you give). After the run, the summary is printed (passed/failed/total, duration) and the report path is shown (e.g. report/report.html).

From code

Use runConfigFile to run a config file and get the result (same shape as the normal test run result).

const { runConfigFile } = require('cstesting');

async function main() {
  const result = await runConfigFile('login.conf');
  console.log('Passed:', result.passed, 'Failed:', result.failed, 'Total:', result.total);
  if (result.failed > 0) {
    for (const { test, error } of result.errors) {
      console.log('  Failed:', test, error.message);
    }
  }
}
main();

You can override headless mode from code:

const result = await runConfigFile('login.conf', { headless: false });

If you pass headless, it overrides the headless / headed lines in the config file.

HTML report

After a config run, the same HTML report is written as for code-based tests:

  • Locationreport/report.html (relative to the current working directory; the report/ folder is created if needed).
  • Content — One row per test case (each # section). You can expand a row to see the list of steps (goto, type username, click submit, etc.). Failed test cases show the error message and stack.
  • Opening the report — The path is printed in the terminal. Open the file in your browser (e.g. start report\report.html on Windows, open report/report.html on macOS).

Locators in config

Locators in config use the same rules as in code. You can use:

SyntaxMeaningExample
#idElement with id="id"#email
.classElement with that class.btn-primary
[name="x"]Attribute name="x"[name="userName"]
[id="x"]Attribute id="x"[id="submit"]
Any [attr="value"]Attribute selector[data-testid="login"]
Full CSS selectorStandard CSSbutton[type="submit"]
XPathXPath expression//button[text()='Submit']

Examples in config:

username:#email=value:john
password:[name="password"]=value:secret
click=button[type="submit"]
click=[name="submit"]

For type steps, the locator should point to an input or focusable element. For click steps, it should point to a clickable element (button, link, etc.). The runner waits for the element to be present (up to a timeout) before acting.

Summary

TopicDescription
FilePlain text file, one step per line; extension .conf or .config.
Sections# Test case name starts a new test case; all steps until the next # belong to it.
Optionsheadless=true/false, headed=true/false — control visible browser.
Stepsgoto:<url>, <label>:<locator>=value:<text>, click=<locator>.
Runnpx cstesting run login.conf or npx cstesting login.conf.
From coderunConfigFile('login.conf') or runConfigFile('login.conf', { headless: false }).
ReportSame as code tests: report/report.html with one row per test case and expandable steps.

Config-driven tests are intended for flows that fit the current step types (goto, type, click). For more complex logic, assertions, or API calls, use the regular test runner and write tests in code (see Writing tests and Page Object Model).

TypeScript

Install

npm install cstesting
npm install -D ts-node typescript

Running

npx cstesting tests/

Test files: You can write tests in .test.ts or .spec.ts. The CLI discovers them the same as .test.js / .spec.js.

Running .ts tests: Install ts-node in your project. If you don't use ts-node, compile TypeScript to JavaScript first (tsc) and run the generated .js files.

Annotations & features

1. Config-file annotations (config-driven tests)

Annotation Description Example
# Test case name Starts a single test case; all following steps belong to it until the next #. Report shows one test per section. # Login Page - Mercury Tours
headed=true Run browser in headed mode (visible window). headed=true
headless=false Same as headed mode. headless=false
goto:<url> Navigate to URL. goto:https://example.com/login
<label>:<locator>=value:<text> Type text into element. username:[name="userName"]=value:mercury
click=<locator> Click element. click=[name="submit"]

Note: More action types (dropdowns, checkboxes, waits, assertions, etc.) are planned.

2. Test-code annotations (describe / it / hooks)

  • describe, it, describe.only, it.only, describe.skip, it.skip
  • beforeAll, afterAll, beforeEach, afterEach
  • step(name) for the HTML report

3. Assertions (expect)

toBe, toEqual, toBeTruthy / toBeFalsy, toBeNull / toBeDefined / toBeUndefined, toThrow, toBeGreaterThan / toBeLessThan, toContain, toHaveLength, expect(x).not.*

4. Other features

  • TypeScript support (types + .test.ts with ts-node)
  • Page Object Model (npx cstesting init)
  • Browser API (CDP-based)
  • HTML report and config run (npx cstesting run login.conf)

API

Test structure

  • describe(name, fn) — define a suite (nested suites supported)
  • it(name, fn) — define a test (async supported)
  • describe.only / it.only — run only this suite/test
  • describe.skip / it.skip — skip this suite/test

Browser API

Use createBrowser() for CDP-based browser automation. Requires Chrome or Chromium.

const { createBrowser, describe, it, expect, beforeAll, afterAll } = require('cstesting');

describe('My site', () => {
  let browser;

  beforeAll(async () => {
    browser = await createBrowser({ headless: true });
  });

  afterAll(async () => {
    await browser.close();
  });

  it('loads the page', async () => {
    await browser.goto('https://example.com');
    const html = await browser.content();
    expect(html).toContain('Example Domain');
  });
});

Actions

Introduction

Playwright can interact with HTML Input elements such as text inputs, checkboxes, radio buttons, select options, mouse clicks, type characters, keys and shortcuts as well as upload files and focus elements.

Text input – ways to handle

CSTesting supports 5 ways to handle text input (typing into inputs):


1. Config file (no code)

In a .conf file, one line per field. Format: <label>:<locator>=value:<text>

# Login Page
username:[name="userName"]=value:mercury
password:[name="password"]=value:secret

Run: npx cstesting run login.conf


2. browser.type(selector, text)

Direct API: pass a CSS selector (or shorthand) and the text.

const { createBrowser } = require('cstesting');
const browser = await createBrowser({ headless: true });
await browser.goto('https://example.com/login');
await browser.type('[name="userName"]', 'mercury');
await browser.type('[name="password"]', 'secret');
await browser.close();

Locator shorthand: name="userName"[name="userName"], id="email"[id="email"], etc.


3. browser.locator(selector).type(text)

Use a locator when you need to chain (e.g. type then press key) or target one of multiple elements.

await browser.goto('https://www.google.com');
const searchBox = browser.locator('[name="q"]');
await searchBox.type('CSTesting');
await searchBox.pressKey('Enter');

When multiple elements match: use .first(), .last(), or .nth(index):

await browser.locator('input').first().type('hello');
await browser.locator('input').nth(1).type('world');

4. browser.getByAttribute(attr, value).type(text)

Type into an element found by attribute (same locator API).

const input = browser.getByAttribute('name', 'userName');
await input.type('mercury');

5. Page Object (wrap in a class)

Centralize selectors and typing in a page class; use it in tests.

pages/LoginPage.js:

class LoginPage {
  constructor(browser) {
    this.browser = browser;
  }
  async typeUsername(value) {
    await this.browser.type('[name="userName"]', value);
  }
  async typePassword(value) {
    await this.browser.type('[name="password"]', value);
  }
  async submit() {
    await this.browser.click('[name="submit"]');
  }
}
module.exports = LoginPage;

Test:

const { createBrowser, describe, it, beforeAll, afterAll } = require('cstesting');
const LoginPage = require('./pages/LoginPage');

describe('Login', () => {
  let browser, page;
  beforeAll(async () => {
    browser = await createBrowser({ headless: true });
    page = new LoginPage(browser);
  });
  afterAll(async () => await browser.close());

  it('logs in', async () => {
    await browser.goto('https://example.com/login');
    await page.typeUsername('mercury');
    await page.typePassword('secret');
    await page.submit();
  });
});

Summary

# Way Use when
1 Config file label:locator=value:text No code; run flows from .conf
2 browser.type(selector, text) Simple one-off typing
3 browser.locator(selector).type(text) Chain with pressKey; use .first()/.nth()
4 browser.getByAttribute(attr, value).type(text) Find by attribute, then type
5 Page Object method Reuse and maintain selectors in one place

CSTesting supports three ways to select an option in a <select> dropdown:


1. By visible text (label)

Match the option by the text the user sees. Use when the visible label is stable and you want tests to read clearly.

const { createBrowser } = require('cstesting');
const browser = await createBrowser({ headless: true });
await browser.goto('https://example.com/form');
// Select the option whose visible text is "Europe"
await browser.select('#country', { label: 'Europe' });
await browser.close();

Locator shorthand: name="country"[name="country"], id="country"#country, etc.


2. By index (0-based)

Select by the position of the option. Use when the order is fixed and you don't care about the exact text or value.

// Select the first option (index 0)
await browser.select('#country', { index: 0 });
// Select the third option (index 2)
await browser.select('#country', { index: 2 });

When multiple <select> elements match the selector, use a locator with .first(), .last(), or .nth(n):

await browser.locator('select.region').first().select({ index: 1 });

3. By value

Select by the option's value attribute. Use when the value is stable (e.g. API or backend contract) and may differ from the visible text.

// Select the option with value="uk"
await browser.select('#country', { value: 'uk' });

4. Multi-select dropdown

For <select multiple>, pass an array of options. The current selection is cleared and replaced with the given set. You can mix label, index, and value in the same call.

// Select multiple options by visible text
await browser.select('#tags', [
  { label: 'JavaScript' },
  { label: 'Testing' },
]);

// Or by value
await browser.select('#tags', [{ value: 'js' }, { value: 'test' }]);

// Or by index (e.g. first and third option)
await browser.select('#tags', [{ index: 0 }, { index: 2 }]);

// Mix: one by label, one by value
await browser.select('#tags', [{ label: 'JavaScript' }, { value: 'e2e' }]);

With a locator: browser.locator('select#tags').select([{ label: 'A' }, { label: 'B' }]). A single change event is dispatched after all options are set.

Checkbox and radio

Use check(selector) and uncheck(selector) for <input type="checkbox"> and <input type="radio">. The element must be an INPUT with type checkbox or radio; otherwise a clear error is thrown. A change and click event are dispatched after setting checked.

Checkbox

// Check a checkbox (e.g. "I agree")
await browser.check('#agree');
// or by name
await browser.check('name="terms"');

// Uncheck
await browser.uncheck('#newsletter');

// When multiple match, use a locator
await browser.locator('input[type="checkbox"]').nth(1).check();

Radio button

Select one option in a group by checking the radio you want. Other radios with the same name are unchecked by the browser.

// Select "Yes"
await browser.check('#choice-yes');
// or by value with a selector that targets that option
await browser.check('input[name="choice"][value="yes"]');

// To switch selection, check another radio (no need to uncheck the previous one)
await browser.check('input[name="choice"][value="no"]');

Note: uncheck on a radio sets that radio to unchecked. Usually you just check another radio in the group instead.

Summary

Action Method Use when
Check browser.check(selector) Check a checkbox or select a radio option
Uncheck browser.uncheck(selector) Uncheck a checkbox
With locator browser.locator(selector).check() / .uncheck() When multiple elements match (use .first(), .nth(n))

Element state: isVisible, isDisabled, isEditable, isSelected

Use these to assert or branch on element state. All return Promise<boolean> and use the same locator rules (strict mode, .first(), .nth(n)).

isVisible(selector)

true if the element exists and is visible: not display:none, not visibility:hidden, opacity > 0, and has non-zero width/height.

const visible = await browser.isVisible('#submit');
expect(visible).toBe(true);

// With locator (e.g. when multiple match)
const firstVisible = await browser.locator('.btn').first().isVisible();

isDisabled(selector)

true if the element has disabled === true (e.g. <input disabled>, <button disabled>).

const disabled = await browser.isDisabled('#submit');
expect(disabled).toBe(false);

isEditable(selector)

true for <input> or <textarea> that is not disabled and not readonly; also true for contenteditable elements.

const canEdit = await browser.isEditable('name="email"');

isSelected(selector)

  • Checkbox / radio: true if checked.
  • <option>: true if that option is selected.
  • <select>: true if the select has at least one selected option (selectedIndex >= 0).
const checked = await browser.locator('#tuesday').isSelected();
expect(checked).toBe(true);

const hasSelection = await browser.isSelected('#country');
const optionSelected = await browser.locator('#country option[value="uk"]').isSelected();

Summary

# Method Use when
1 browser.isVisible(selector) Check if element exists and is visible
2 browser.isDisabled(selector) Check if element is disabled
3 browser.isEditable(selector) Check if input/textarea/contenteditable is editable
4 browser.isSelected(selector) Check if checkbox/radio/option is selected, or select has selection

You can also use a locator: browser.locator(selector).isVisible(), .isDisabled(), .isEditable(), .isSelected() (e.g. with .first() or .nth(n) when multiple elements match).

Locators

Introduction — Locators are how you find element(s) on the page. Every action (click, type, select, check, etc.) and state check (isVisible, isDisabled, isSelected) takes a selector or uses a locator. When you use a locator, the element is resolved at the moment of the action, so if the DOM changes (e.g. re-render), the next action uses the current match.

Quick guide — CSTesting supports:

Use case How in CSTesting
By role (e.g. button with text) XPath: //button[text()="Sign in"], or CSS button + .first() if needed
By label (form control) name="userName" (shorthand) or getByAttribute('name', 'userName'); for label text use XPath: //label[contains(.,"Password")]/following-sibling::input or similar
By placeholder placeholder="name@example.com" (shorthand) or getByAttribute('placeholder', '...')
By text XPath: //*[text()="Welcome"] or //*[contains(text(),"Welcome")]
By alt text (images) getByAttribute('alt', 'logo description') or [alt="..."]
By title getByAttribute('title', '...') or [title="..."]
By test id getByAttribute('data-testid', 'submit-btn') or [data-testid="submit-btn"]

Selector shorthand (for locator(), click(), type(), and all actions):

  • name="userName"[name="userName"]
  • id="userName"[id="userName"]
  • class="userName".userName
  • Any attribute: placeholder="Enter Name"[placeholder="Enter Name"]
  • Anything else → CSS selector
  • Selector starting with / or (XPath (e.g. //button[text()="Sign in"], (//div)[1])

Strict mode — If the selector matches 0 elements, an error is thrown. If it matches 2 or more, an error suggests using .first(), .last(), or .nth(n) so the intent is explicit.

When multiple elements match — Use a locator and narrow:

  • browser.locator('button').first().click() — first match
  • browser.locator('button').last().click() — last match
  • browser.locator('button').nth(1).click() — second match (0-based)

Chaining — You can chain from a frame: browser.frame('iframe#form').locator('input').type('hello'). Locators support .click(), .type(text), .select(option), .check(), .uncheck(), .pressKey(key), .isVisible(), .isDisabled(), .isEditable(), .isSelected(), .textContent(), .getAttribute(name).

Example — Locate by label-like attribute, then act:

await browser.getByAttribute('name', 'userName').type('mercury');
await browser.getByAttribute('name', 'password').type('secret');
await browser.locator('//button[text()="Sign in"]').click();
const visible = await browser.locator('//*[contains(text(),"Welcome")]').isVisible();
expect(visible).toBe(true);

Locate by role (button, checkbox, etc.) — Use XPath or CSS that matches the element and its accessible name:

// Button with exact text
await browser.locator('//button[text()="Sign in"]').click();

// Checkbox with label (match input near label text)
await browser.locator('//label[contains(.,"Subscribe")]//input').check();

// Heading
const heading = await browser.locator('//h3[text()="Sign up"]').textContent();

Locate by placeholder — Use the shorthand or getByAttribute:

await browser.locator('placeholder="name@example.com"').type('user@test.com');
// or
await browser.getByAttribute('placeholder', 'name@example.com').type('user@test.com');

Locate by text — Use XPath for elements by their text content:

await expect(await browser.locator('//*[contains(text(),"Welcome, John")]').isVisible()).toBe(true);

Locate by test id — Prefer data-testid (or a custom attribute) for stable selectors:

await browser.getByAttribute('data-testid', 'submit-btn').click();

Frames

Introduction — A page has one main frame; page-level interactions (click, type, etc.) run in the main frame. A page can have additional frames attached via <iframe>. You can get a frame handle and interact inside the frame without switching the main page. CSTesting supports same-origin iframes only (uses the frame's contentDocument).

Locate element inside a frame — Use browser.frame(iframeSelector).locator(selector) to get a locator that runs inside the frame, then call .type(), .click(), etc.:

const frame = browser.frame('.frame-class');   // or iframe[name="frame-login"], iframe#myframe
await frame.locator('[name="userName"]').type('John');
await frame.locator('[name="password"]').type('secret');
await frame.locator('//button[text()="Sign in"]').click();

Or use the frame handle's methods directly with a selector:

await frame.type('[name="userName"]', 'John');
await frame.type('[name="password"]', 'secret');
await frame.click('//button[text()="Sign in"]');

Frame objects — Get a frame with browser.frame(selector). The selector can target the iframe by id, name, class, or any CSS/XPath:

// By frame's name attribute
const frame = browser.frame('iframe[name="frame-login"]');
// or XPath: browser.frame('//iframe[@name="frame-login"]')

// By id or class
const frame = browser.frame('iframe#myframe');
const frame = browser.frame('.frame-class');

// Interact inside the frame (same API as page: click, type, locator, evaluate, content, select, check, etc.)
await frame.type('[name="username-input"]', 'John');
await frame.click('button');

Nested frames — Use .frame(selector) on a frame handle to target an iframe inside that frame:

const outer = browser.frame('iframe#outer');
const inner = outer.frame('iframe#inner');
await inner.click('button');
// or in one go: browser.frame('iframe#outer').frame('iframe#inner').click('button');

Late-loading inner frame — If the inner iframe loads after the page, use frame.waitForSelector(selector, { timeout }) before interacting:

const inner = browser.frame('iframe#outer').frame('iframe#inner');
await inner.waitForSelector('button', { timeout: 10000 });
await inner.click('button');

Dialogs

Introduction — CSTesting can interact with JavaScript dialogs: alert, confirm, prompt, and beforeunload. Dialogs are handled in the same CDP session (no switchTo().alert()). You register a dialog handler with browser.setDialogHandler(handler) so that when a dialog opens, your handler decides whether to accept or dismiss it (and, for prompt, what text to send).

alert(), confirm(), prompt() — By default, all dialogs are auto-dismissed (accepted): you don't have to set a handler. To control the behaviour, register a handler before the action that triggers the dialog. The handler receives { type, message } and must return { accept: true } or { accept: false } (and for prompt, optional promptText):

browser.setDialogHandler(({ type, message }) => ({ accept: true }));
await browser.locator('//button[text()="Submit"]').click();

Note — The dialog handler must handle the dialog (return accept/dismiss). Dialogs are modal and block page execution until handled. If your handler does not respond (e.g. you only log the message), the action that triggered the dialog will stall and never resolve.

Wrong — Do not only log and leave the dialog unhandled:

// WRONG: click will hang because the dialog is never accepted/dismissed
browser.setDialogHandler(({ message }) => console.log(message));
await browser.click('button');  // stalls

Correct — Always return a result:

browser.setDialogHandler(({ type, message }) => ({
  accept: true,
  promptText: type === 'prompt' ? 'my value' : undefined,
}));

Dismiss (Cancel) — Return accept: false to dismiss confirm/prompt:

browser.setDialogHandler(() => ({ accept: false }));

Reset to defaultbrowser.setDialogHandler(null) restores default behaviour (accept all; prompt gets '').

beforeunload — When the page fires a beforeunload dialog (e.g. on leave), the handler receives type === 'beforeunload'. Return { accept: true } to accept or { accept: false } to dismiss:

browser.setDialogHandler(({ type }) => ({
  accept: type !== 'beforeunload',   // dismiss beforeunload, accept others
}));

Print dialogs — To assert that window.print() was triggered (e.g. after clicking "Print"), you can replace window.print and then wait for it to be called:

await browser.goto('https://example.com/page-with-print');
// Replace window.print so we can detect when it's called
await browser.evaluate(`
  window._printCalled = false;
  window.print = () => { window._printCalled = true; };
`);
await browser.locator('//button[text()="Print it!"]').click();
// Poll until print was invoked (or use a short sleep and then check)
const printCalled = await browser.evaluate('window._printCalled');
expect(printCalled).toBe(true);

For a more robust wait, poll in a loop with browser.evaluate('window._printCalled') and browser.sleep(100) until true or a timeout.


New tabwaitForNewTab() returns a TabHandle (page-like object). Use browser for the parent and newTab for the new tab without switching:

const [newTab] = await Promise.all([
  browser.waitForNewTab(),
  browser.click('a[target="_blank"]'),
]);

// Parent: use browser (stays on parent)
const parentTitle = await browser.evaluate('document.title');

// New tab: use newTab handle (same API: evaluate, click, content, locator, etc.)
const newTabTitle = await newTab.evaluate('document.title');
await newTab.click('button');
// Optional: close only the new tab's connection
await newTab.close();

Switching tabs — To move the main browser to another tab: await browser.switchToTab(1) or await browser.switchToTab(tabId). List tabs: const tabs = await browser.getTabs();

Frames — Use browser.frame(iframeSelector) to get a frame handle; then frame.click(), frame.locator(...).type(), frame.evaluate(), etc. Same-origin iframes only. See the Frames section for nested frames and late-loading inner frames.

Introduction — CSTesting can navigate to URLs and handle navigations caused by page interactions (e.g. clicking a link or submitting a form).

Basic navigation — The simplest form is opening a URL:

await browser.goto('https://example.com');

This loads the page and waits for the load event. The load event fires when the document and its dependent resources (stylesheets, scripts, iframes, images) have loaded.

Note — If the page does a client-side redirect before load, browser.goto() waits for the redirected page to fire the load event.

When is the page loaded? — Modern pages often do more after the load event: they fetch data lazily, hydrate UI, or load extra scripts. There is no single definition of "fully loaded"; it depends on the app. When can you start interacting?

In CSTesting you can interact at any time. Actions auto-wait for the target element to become actionable (visible, stable, enabled). You don't have to add an explicit "wait for page ready" in the general case.

await browser.goto('https://example.com');
await browser.locator('//*[contains(text(),"Example Domain")]').click();  // click auto-waits for the element

CSTesting behaves like a fast user: as soon as the element is ready, it acts. You usually don't need to worry about every resource having loaded.

Hydration — Sometimes you'll see a click or typed text that seems to have no effect (or the text disappears). A common cause is poor page hydration: the server sends static HTML, then JavaScript runs and "hydrates" the page. If you interact before hydration finishes, the button may be visible and clickable in the DOM but its listeners aren't attached yet, so the click does nothing or the input is reset.

A simple check: in Chrome DevTools, enable "Slow 3G" in the Network panel and reload. If clicks are ignored or typed text is cleared, the page likely has a hydration timing issue. The fix is on the app side: disable interactive controls until after hydration, or ensure they are only enabled when the page is fully functional.

Waiting for navigation — A click (e.g. submit, link) can trigger a navigation. You can wait for the new page in three ways:

  1. browser.waitForURL(urlOrPattern, { timeout? }) — Wait until the page URL matches. Use a substring, a glob like '**/login', or a RegExp.
  2. browser.waitForLoad() — Wait for the next load event (full document load after navigation).
  3. browser.waitForSelector(selector, { timeout }) — Wait for an element that appears only on the new page (e.g. a login form or a success message).
await browser.locator('//button[text()="Click me"]').click();
await browser.waitForURL('**/login');   // wait until URL contains /login (glob: ** becomes .*)

// Or by substring or RegExp:
await browser.waitForURL('/dashboard');
await browser.waitForURL(/\/login$/);
await browser.locator('//button[text()="Submit"]').click();
await browser.waitForLoad();   // wait for the navigated page to load
// or wait for something that only exists on the new page:
await browser.waitForSelector('h1', { timeout: 10000 });

Navigation and loading — Showing a new document involves navigation and loading.

  • Navigation starts when the URL changes or you interact (e.g. click a link). It can fail (e.g. DNS error) or turn into a download. When the response headers are in and session history is updated, the navigation is committed; only then does loading start.
  • Loading is receiving the response body, parsing the document, running scripts, and firing events:
    • The page URL is set to the new URL
    • Document content is loaded and parsed
    • DOMContentLoaded fires (when the HTML is parsed; scripts may still run)
    • Scripts run and resources (styles, images) load
    • The load event fires when the document and its subresources are done

browser.goto(url) waits for the load event. After a click that navigates, use browser.waitForURL('**/login'), browser.waitForLoad(), or waitForSelector to wait for the new page.

Annotations and test control

Introduction

CSTesting lets you focus or skip tests and suites so you can run a subset of tests or temporarily disable some. Skipped tests are counted and shown in the HTML report.

This page describes what is supported. Features like tags, custom annotations, conditional skip, or "expected to fail" are not implemented in the current version; the table at the end summarizes supported vs not supported.

You will learn

  • How to focus tests (run only certain tests)
  • How to skip tests or suites
  • What is not yet supported (tags, annotations, conditional skip, etc.)

Focus a test or suite

When you mark tests or suites with only, only those run. All others are skipped. Use this to quickly run one test or one group while developing.

Run only one test

const { describe, it, expect, createBrowser, beforeAll, afterAll } = require('cstesting');

describe('My suite', () => {
  it('this test runs', async () => {
    expect(1).toBe(1);
  });

  it.only('only this test runs', async () => {
    expect(2).toBe(2);
  });

  it('this test is skipped', async () => {
    expect(3).toBe(3);
  });
});

When you use it.only, only that test runs in the whole project (across all loaded files). The other tests in the same suite are skipped.

Run only one suite

describe('this suite runs', () => {
  it('test one', () => {});
  it('test two', () => {});
});

describe.only('only this suite runs', () => {
  it('test a', () => {});
  it('test b', () => {});
});

describe('this suite is skipped', () => {
  it('test x', () => {});
});

When you use describe.only, only that suite's tests run. All other suites (and their tests) are skipped.

You can combine describe.only and it.only: if any suite or test has only, only the ones marked with only run.

Skip a test or suite

Mark a test or suite as skip so it is not run. Skipped tests are still counted and appear in the terminal summary and in the HTML report under "Skipped".

Skip one test

it.skip('skip this test', async () => {
  // This test is not run.
});

Skip a whole suite

describe.skip('skip this suite', () => {
  it('test one', () => {});
  it('test two', () => {});
});

Use skip when a test is broken, not applicable yet, or you want to disable it without deleting it.

Report

  • Focus (only): Tests/suites without only are skipped when any only is present. They appear in the report as Skipped.
  • Skip: Tests/suites marked with skip are not run and appear as Skipped in the report.

The HTML report has a "Skipped" section listing all skipped tests (both from only and from skip). The terminal output shows: Passed: X Failed: Y Skipped: Z Total: N.

What is not supported (current version)

The following are not implemented in CSTesting. Use the workarounds below if you need similar behavior.

FeatureSupported?Workaround
Focus (it.only, describe.only)Yes
Skip (it.skip, describe.skip)Yes
Expected to fail (run test and expect it to fail)NoRun the test normally; if it fails, that's the outcome. No "must fail" annotation.
Fixme (skip because broken/slow, with message)NoUse it.skip('message') to skip; the message is in the test name only.
Slow (e.g. triple timeout)NoNo per-test timeout or "slow" annotation. Use a longer global timeout or split slow tests.
Tags (e.g. @fast, @slow in title or options)NoPut keywords in the test/suite name and run by file or folder (e.g. npx cstesting tests/fast/). No --grep by tag.
Custom annotations (type + description, e.g. issue URL)NoNo annotation API; report does not show custom annotations. Add context in the test name or in step() text.
Conditional skip (e.g. skip if browser is Firefox)NoCSTesting runs one browser (Chrome). Use a normal if and return early, or skip the whole suite with describe.skip if needed.
Runtime annotations (add annotation during test)NoNo test.info() or similar. Use step('description') to add steps that appear in the report.
Filter by tag from CLI (--grep, --grep-invert)NoFilter by path/pattern only: npx cstesting path/to/file.test.js or npx cstesting tests/smoke/.

Summary

ActionSyntaxEffect
Focus one testit.only('name', fn)Only this test runs (others skipped).
Focus one suitedescribe.only('name', fn)Only tests in this suite run.
Skip one testit.skip('name', fn)This test is not run (reported as skipped).
Skip one suitedescribe.skip('name', fn)No tests in this suite run (all reported as skipped).

Skipped tests (from only or skip) are included in the total count and listed in the HTML report under "Skipped". Tags, custom annotations, conditional skip, and grep by tag are not available in the current version.