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 isundefined, so the project should check it at the start and stop with a clear message instead of failing later on a broken address such asundefined/login. Ctrl+Cstops 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 testNode.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"
}
}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:smokeHow 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)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.
- 1CommandTyped in the project folder, or run by the CI step, with environment variables such as
BASE_URLset firstnpx playwright test --grep @smoke - 2Runner startsReads the config, discovers files named
*.spec.ts, keeps only the tests the filter matches - 3Tests runEvery
test()inside itsdescribeblock, headless unless--headedor--debugwas given - 4Report writtenConsole summary; HTML report in
playwright-report/; screenshots and traces of failures intest-results/; JUnit XML for CI - 5Exit code
0when 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
- 6CI job greenThe step passed; the pipeline can keep the report as an artifact and the change may mergePass
- Python:
pip install -r requirements.txtinstalls the dependencies;pytest tests/test_login.py -k wrong_passwordruns one file filtered by a keyword in the test name and-m smokeselects a marker; pytest exits0when the selected tests ran and none failed,1when a test failed and5when no test was collected. - Java: Maven reads
pom.xml;mvn testruns the suite andmvn -Dtest=LoginTest testruns 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%
1Common 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 installin CI or on a new machine, which can change versions and the lock file, instead ofnpm 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,--debugor--uiin 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
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.
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)- Write the install commands and say why not
npm install. - Write the command that runs one file in headed mode.
- Write the tagged-set command with the environment variable in bash, then in PowerShell, and count how many tests will run.
- Read the output: how many tests ran, the exit code, the CI result and what to check.
Show the 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
- Playwright DocumentationOfficial documentation for Playwright, an end-to-end test framework that drives Chromium, WebKit and Firefox.
- Continuous Integration (Martin Fowler)The widely referenced description of continuous integration: integrate often, keep the build self-testing and fast, and fix a broken build immediately.
- GitHub Actions documentationHow workflows, jobs, runners and artifacts are defined in GitHub Actions — where automated tests run on each push or pull request.
- Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0ISTQB's advanced syllabus on designing, implementing and maintaining test automation solutions and their architecture.
Practise with Mutqan tools
Validate JSON syntax and get precise error locations with helpful fix suggestions.
Test regular expressions live with match highlighting, capture groups and replace preview.
Compare two texts line by line or word by word and highlight differences.
Format and validate YAML with consistent indentation and quoting.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.