The Command Line and Test Runners

The command line is a text window where you type a command and a program answers in text. Test automation lives there: the test runner, the program that finds your test files, executes them, prints the result and writes a report, is started with a command such as npx playwright test, and the CI server starts it the same way, with a command and with nobody watching.

This lesson covers the small set of commands you need: knowing which folder you are in, installing a project with npm, running all tests, one file or a filtered set in headed, headless or debug mode, reading the output, and the one number that decides whether a CI job is green or red: the exit code.

Why it matters

In the previous lesson you read a test file; nothing happens until something runs it. A tester who cannot run the suite from the terminal depends on others to learn whether the product is broken, cannot reproduce a red pipeline on their own machine and cannot narrow a two-hour run down to the one test that matters.

The terminal is also the honest version of the truth: a green badge on a dashboard means only that a command returned exit code 0. Once you see the chain of command, runner, tests, report and exit code, you can tell a product defect from a broken installation, a wrong folder or a filter that matched nothing, and give developers the exact command that reproduces a failure.

The terminal: where you are and what you run

Every command runs inside a working directory, the folder the terminal is currently in. pwd prints it, ls lists its contents (dir in the old Windows command prompt), cd tests moves into a sub-folder and cd .. moves one level up. A path is relative to that folder (tests/login.spec.ts) or absolute from the root of the disk (/home/sara/shop-tests, C:\Users\sara\shop-tests). Many "command not found" and "no tests found" surprises are just a terminal sitting in the wrong folder.

A command is a program name followed by arguments and options: in npx playwright test tests/login.spec.ts --headed, npx is the program, the file is an argument and --headed an option. The shell, the program that reads what you type, finds programs through the PATH environment variable, a list of folders it searches for a program with that name. Environment variables are named values that every program receives from the shell that started it; a value you set lasts only for that terminal window. Test projects keep everything that changes between machines in them, such as the URL under test or a password, so the test code stays the same.

  • bash, zsh and Git Bash: export BASE_URL=https://staging.shop.example, or for one command only: BASE_URL=https://staging.shop.example npx playwright test.
  • PowerShell: $env:BASE_URL = 'https://staging.shop.example'; command prompt: set BASE_URL=https://staging.shop.example.
  • In Node.js the value arrives as process.env.BASE_URL; when it is missing it is undefined, so the project should check it at the start and stop with a clear message instead of failing later on a broken address such as undefined/login.
  • Ctrl+C stops a running command; the Tab key completes file names.
$ pwd                        # where am I?  (PowerShell: pwd works too; cmd.exe: cd)
/home/sara/shop-tests
$ ls                         # what is here?  (cmd.exe: dir)
node_modules  package.json  package-lock.json  playwright.config.ts  tests
$ cd tests && ls
checkout.spec.ts  login.spec.ts
$ cd ..
$ export BASE_URL=https://staging.shop.example   # PowerShell: $env:BASE_URL = '...'
$ npx playwright test

Node.js, npm, npx and the two package files

JavaScript runners such as Playwright Test, Jest and Vitest run on Node.js. Install the current LTS (long-term support) release from nodejs.org and check with node --version and npm --version. npm, the package manager that ships with Node, downloads the libraries a project depends on into node_modules. npx runs a command-line program that one of those packages provides, here playwright, without a global installation; if the package is not in the project, npx downloads it into a cache first, asking for confirmation when the terminal is interactive.

Two files describe the project. package.json is written by people: scripts are short names for long commands, so npm test runs whatever the test script says, and devDependencies lists packages with version ranges, where ^1.47.0 means any 1.x release from 1.47.0 upwards. package-lock.json is written by npm: the exact version of every package and sub-package that was installed. npm install treats the lock file as a starting point: when package.json has changed or there is no lock file, it resolves the ranges again, can pick newer versions and rewrites the lock file. npm ci installs exactly what the lock file says, removes an existing node_modules first and stops with an error when the two files disagree. That is why CI, and anyone setting up the project fresh, should run npm ci. It does not download the browsers Playwright drives: npx playwright install does that after Playwright is installed or upgraded, and on a fresh Linux CI machine npx playwright install --with-deps also adds the system libraries they need.

{
  "name": "shop-tests",
  "scripts": {
    "test": "playwright test",
    "test:smoke": "playwright test --grep @smoke"
  },
  "devDependencies": {
    "@playwright/test": "^1.47.0"
  }
}

Try it: JSON Validator

Worked example: one login suite, several ways to run it

The project shop-tests has tests/login.spec.ts with three tests, two of them tagged @smoke, and tests/checkout.spec.ts with four more. These are the Playwright Test commands; other runners have the same shapes with different spellings.

-g (--grep) takes a regular expression, a text pattern in which a plain word is the simplest case, and matches it against the full title, describe names included, so -g Login runs the whole group; a tag is a word such as @smoke filtered the same way. In PowerShell write the tag in quotes, --grep '@smoke': a bare @smoke is read as a PowerShell variable and the filter is lost. Headless, the default, runs the browser without a window. --headed shows the window, --debug opens the Playwright Inspector with the test paused so you can step through it action by action, and --ui lets you watch and re-run. All three are for your machine; CI has no screen.

Goal                           Command
All tests                      npx playwright test
One file                       npx playwright test tests/login.spec.ts
By title (regular expression)  npx playwright test -g "wrong password"
By tag                         npx playwright test --grep @smoke
Headed: watch the browser      npx playwright test --headed
Debug: step through actions    npx playwright test tests/login.spec.ts --debug
One browser project only       npx playwright test --project=chromium
Through package.json scripts   npm test          npm run test:smoke

Try it: Regex Tester

How the runner finds tests and what it prints

The runner does not run "the project"; it discovers files. Playwright Test looks in the configured testDir for files whose names end, by default, in suffixes such as .spec.ts, .spec.js, .test.ts or .test.js, loads them and collects every test() call, grouped by test.describe() blocks; Jest and Vitest use the same structure with describe and it or test, Mocha with describe and it. A file that does not match the pattern is silently ignored, a common reason a new test "did not run".

Read the output top to bottom: the header counts the tests found and the workers, the parallel processes that run them; each line is one test with its status, file, line number and title; failures are repeated at the end with the assertion message and a stack trace; the last lines count failed, flaky (failed, then passed on a retry), skipped and passed. The html reporter, when the configuration turns it on, writes to playwright-report/ (open it with npx playwright show-report), screenshots and traces of failed tests, when the configuration turns them on, go to test-results/, and the junit reporter writes XML, usually to a file named in the configuration, that CI servers turn into a per-test view.

// tests/login.spec.ts — what the runner collects
import { test, expect } from '@playwright/test';

test.describe('Login', () => {
  test('valid login reaches dashboard', { tag: '@smoke' }, async ({ page }) => {
    // arrange, act, assert
  });
  test('wrong password shows error', { tag: '@smoke' }, async ({ page }) => {
    // ...
  });
  test('locked account is refused', async ({ page }) => { /* ... */ });
});

// npx playwright test tests/login.spec.ts — the list reporter
Running 3 tests using 1 worker

  ✓  1 [chromium] › login.spec.ts:4:7 › Login › valid login reaches dashboard (2.1s)
  ✘  2 [chromium] › login.spec.ts:7:7 › Login › wrong password shows error (1.4s)
  ✓  3 [chromium] › login.spec.ts:10:7 › Login › locked account is refused (1.2s)

  1) [chromium] › login.spec.ts:7:7 › Login › wrong password shows error

    Error: expect(locator).toHaveText(expected) failed
    Expected string: "Wrong e-mail or password"
    Received string: "Invalid credentials"

  1 failed
    [chromium] › login.spec.ts:7:7 › Login › wrong password shows error
  2 passed (5.0s)

Try it: Text Diff

Exit codes: how CI knows the result

Every program ends with an exit code, a number the operating system hands back to whoever started it. By convention 0 means success and anything else means failure. A test runner exits with 0 only when no test failed; a failed test, an error in the configuration file, a filter that matched nothing or a browser that is not installed all give a non-zero code. Right after a command, echo $? prints it in bash, $LASTEXITCODE in PowerShell and %ERRORLEVEL% in the command prompt.

CI servers such as GitHub Actions, GitLab CI and Jenkins do not read colours: they run each step as a command and mark the step failed when its exit code is non-zero, which stops the job, turns it red and, where the team has made the check required, blocks the merge. That is the whole contract. It also means a runner that swallows failures and exits 0 gives a green job for a broken product, and a test that fails only now and then turns the job red even when the product works. The steps themselves live in a pipeline file in the repository, a YAML workflow in .github/workflows/ for GitHub Actions or .gitlab-ci.yml for GitLab CI (Jenkins uses a Jenkinsfile), where every run: or script: entry is a command like the ones in this lesson.

One command becomes a verdict: the runner discovers and runs the tests, writes the report, and its exit code, 0 or non-zero, is what turns the CI job green or red.
  1. 1CommandTyped in the project folder, or run by the CI step, with environment variables such as BASE_URL set firstnpx playwright test --grep @smoke
  2. 2Runner startsReads the config, discovers files named *.spec.ts, keeps only the tests the filter matches
  3. 3Tests runEvery test() inside its describe block, headless unless --headed or --debug was given
  4. 4Report writtenConsole summary; HTML report in playwright-report/; screenshots and traces of failures in test-results/; JUnit XML for CI
  5. 5Exit code0 when no test failed; non-zero when a test failed or the runner could not run at allecho $?

    Branch: Non-zero: the step fails, the job turns red, a required check blocks the merge; open the reportFail

    Gate: exit code 0

  6. 6CI job greenThe step passed; the pipeline can keep the report as an artifact and the change may mergePass
  • Python: pip install -r requirements.txt installs the dependencies; pytest tests/test_login.py -k wrong_password runs one file filtered by a keyword in the test name and -m smoke selects a marker; pytest exits 0 when the selected tests ran and none failed, 1 when a test failed and 5 when no test was collected.
  • Java: Maven reads pom.xml; mvn test runs the suite and mvn -Dtest=LoginTest test runs one class; a failing test fails the build with a non-zero exit code.
$ npx playwright test --grep @smoke
  1 failed
  2 passed (6.8s)
$ echo $?                    # PowerShell: $LASTEXITCODE    cmd.exe: echo %ERRORLEVEL%
1

Try it: YAML Formatter

Common mistakes

  • Running the command from the home folder instead of the project and reading "command not found" or "no tests found" as a product problem.
  • Using npm install in CI or on a new machine, which can change versions and the lock file, instead of npm ci.
  • Filtering with a title or tag that matches fewer tests than you meant, for example because some tests carry a misspelt tag, and reporting the green run as the whole set passed; always compare the test count in the header with what you expected.
  • Leaving --headed, --debug or --ui in a CI command, where there is no screen and the job hangs or fails.
  • Judging a run by the last line or the colour and ignoring skipped tests, flaky retries and the exit code.
  • Hard-coding the staging URL or credentials in test files instead of environment variables, and then committing them.

The QA perspective

On a new team your first automation task is usually to make the suite run on your machine: clone the repository, read the scripts in package.json and the README, run npm ci and npx playwright install, then the smoke set, and compare your result with the latest CI run. The command CI uses is the one that counts; find it in the pipeline file and use the same one locally.

When a pipeline goes red, open the job log and find the step that failed and its exit code before you look at any test. Record in the ticket the exact command, the environment variables without their secret values, the commit, the browser project and a link to the report the job kept as an artifact (a file saved with the run), so a developer can run the same thing. Ask the team which tags exist, where reports go and who looks at them.

Practice exercise

Write the commands for a CI-ready smoke run

Your task

You join the shop-tests team. Its repository is shown below. Without running anything, write the commands a colleague would type on a new machine to install the dependencies exactly as locked and the browsers, run only the checkout file with the browser visible, and run only the tests tagged @smoke against the staging site through the BASE_URL variable.

Then read the output of the last CI run and state the exit code, whether the job was green, and what you would check before trusting it.

Material
shop-tests/
  package.json          scripts: "test": "playwright test"
  package-lock.json
  playwright.config.ts  testDir: './tests', one project (chromium),
                        baseURL: process.env.BASE_URL
  tests/
    login.spec.ts       3 tests, 2 tagged @smoke
    checkout.spec.ts    4 tests, 1 tagged @smoke

Last CI run of npm test, final lines of the log:
  1 skipped
  6 passed (12.3s)
Steps
  1. Write the install commands and say why not npm install.
  2. Write the command that runs one file in headed mode.
  3. Write the tagged-set command with the environment variable in bash, then in PowerShell, and count how many tests will run.
  4. Read the output: how many tests ran, the exit code, the CI result and what to check.
Show the model answer
Model answer

npm ci: it installs exactly what package-lock.json says, stops if the lock file disagrees with package.json and upgrades nothing by accident. npm install may resolve the ranges to newer versions and rewrite the lock file, so your colleague would test something CI does not. Then npx playwright install downloads the browsers, which npm ci does not.

npx playwright test tests/checkout.spec.ts --headed.

bash: BASE_URL=https://staging.shop.example npx playwright test --grep @smoke; PowerShell: $env:BASE_URL = 'https://staging.shop.example' and then npx playwright test --grep '@smoke', with the tag in quotes because PowerShell reads a bare @smoke as a variable. Three tests run: two from the login file and one from checkout. With three browser projects in the configuration the same command would give nine runs.

All 7 tests of the suite were found: 6 ran and passed and 1 was skipped. There is no failed line, so the exit code was 0 and the job was green. But skipped is not passed: find why it was skipped (test.skip, test.fixme or a condition on the environment), check whether the skipped test is one of the smoke tests, and since when it has been skipped.

Frequently asked questions

Do I need to learn bash if I work on Windows?

PowerShell is the default on Windows and most of this lesson (cd, ls, pwd, npm, npx) works there unchanged; the main differences are environment variables ($env:NAME), the exit code ($LASTEXITCODE) and quoting: an argument that starts with @, such as a tag, needs quotes ('@smoke'). Git Bash gives you a bash shell on Windows. Many CI pipelines run on Linux machines, so their commands are often written in bash and being able to read them pays off.

What is the difference between npm test and npx playwright test?

npm test runs the test script in package.json, which may add options or variables; npx playwright test runs the runner directly with exactly the options you type. Both end in the same runner, so read the script to know what actually ran.

Why did CI fail when every test passed on my machine?

CI usually runs on a different operating system, with npm ci and the lock file, headless, freshly installed browsers and other environment variables. Find the step that failed first: an install or browser download failure comes before any test. Then compare environment variables and the versions in the lock file, and run locally the exact command CI runs.

Does a non-zero exit code always mean a failed test?

No. An error in the configuration file, a syntax error in a test file or a filter that matched nothing exits non-zero before a single test runs, and a browser that is not installed makes every test fail at launch, before it reaches the product. Read the first error line of the log and the number of tests that ran before you open a bug report.

References

Practise with Mutqan tools

Validate JSON syntax and get precise error locations with helpful fix suggestions.

JSON & Data

Test regular expressions live with match highlighting, capture groups and replace preview.

Utilities

Compare two texts line by line or word by word and highlight differences.

Formatting

Format and validate YAML with consistent indentation and quoting.

Formatting

Go deeper

Your progress on this topic

Part of the QA Roadmap