Regex for Testers
Regular expressions are the universal pattern language for QA — useful for validating input fields, asserting log lines, matching URLs, parsing API responses, and writing tolerant test assertions. This sheet covers the syntax most testers actually use, plus a library of ready-to-paste patterns for common QA scenarios.
All examples use the JavaScript / ECMAScript flavour, which is what Cypress, Playwright, and most testing libraries run on.
Basics
Literal matching
A regex without metacharacters matches itself.
const re = /hello/;
re.test('hello world'); // true
re.test('Hello world'); // false — case-sensitive by defaultThe dot . — any character
Matches any single character except newlines (unless the s flag is set).
/c.t/.test('cat'); // true
/c.t/.test('cot'); // true
/c.t/.test('ct'); // false — needs exactly one char in betweenAnchors ^ and $
^ anchors the match to the start of the string (or line, with m flag). $ anchors to the end.
/^hello/.test('hello world'); // true
/^hello/.test('say hello'); // false
/world$/.test('hello world'); // true
/^hello$/.test('hello'); // true — exact match
/^hello$/.test('hello world'); // falseWord boundaries \b
Matches the position between a word character and a non-word character. Useful when you want whole-word matches.
/\bcat\b/.test('the cat sat'); // true
/\bcat\b/.test('category'); // false — 'cat' is part of a word
/\bcat\b/.test('cat-like'); // true — hyphen is a word boundaryCharacter classes
Custom classes [...]
Match any one character from the set.
/[abc]/.test('apple'); // true — matches 'a'
/[0-9]/.test('order 42'); // true
/[a-zA-Z]/.test('Hello'); // true
/[a-zA-Z0-9_-]/ // alphanumeric plus _ and -Negation [^...]
Match any one character NOT in the set.
/[^0-9]/.test('123'); // false — only digits
/[^aeiou]/.test('rhythm'); // true — 'r' is not a vowelPredefined classes
| Pattern | Matches | Equivalent |
|---|---|---|
\d | Any digit | [0-9] |
\D | Any non-digit | [^0-9] |
\w | Any word character | [A-Za-z0-9_] |
\W | Any non-word character | [^A-Za-z0-9_] |
\s | Any whitespace | [ \t\n\r\f\v] |
\S | Any non-whitespace | [^ \t\n\r\f\v] |
/\d{3}/.test('order 42'); // false — only 2 digits
/\d{3}/.test('order 142'); // true
/\w+/.test('hello_world'); // trueQuantifiers
Basic quantifiers
| Pattern | Matches |
|---|---|
* | 0 or more of the preceding element |
+ | 1 or more |
? | 0 or 1 (optional) |
{n} | Exactly n |
{n,} | n or more |
{n,m} | Between n and m (inclusive) |
/ab*/.test('a'); // true — zero b's allowed
/ab+/.test('a'); // false — at least one b required
/colou?r/.test('color'); // true — 'u' is optional
/colou?r/.test('colour'); // true
/\d{4}/.test('2025'); // true — exactly 4 digits
/\d{4,}/.test('20250'); // true — 4 or more
/\d{2,4}/.test('1'); // false — at least 2 requiredGreedy vs lazy
By default quantifiers are greedy — they match as much as possible. Add ? to make them lazy (match as little as possible).
const html = '<b>bold</b> and <i>italic</i>';
html.match(/<.+>/); // greedy: matches '<b>bold</b> and <i>italic</i>'
html.match(/<.+?>/); // lazy: matches '<b>'
html.match(/<.+?>/g); // all tags: ['<b>', '</b>', '<i>', '</i>']This single distinction trips up more people than any other regex feature. When pulling structured fragments out of a longer string, you almost always want lazy.
Groups & alternation
Capturing group (...)
Wraps a sub-pattern. The matched substring is captured and accessible by index.
const m = 'order #1042 placed'.match(/order #(\d+)/);
m[0]; // 'order #1042' — full match
m[1]; // '1042' — first capture groupNon-capturing group (?:...)
Groups for quantification or alternation, but doesn't capture. Slightly faster and keeps your indexes clean.
const m = 'foo123bar'.match(/(?:foo)(\d+)(?:bar)/);
m[1]; // '123' — only the digits are capturedAlternation |
/cat|dog/.test('I have a cat'); // true
/(cat|dog)/.exec('I have a dog'); // captures 'dog'
/^(GET|POST|PUT|DELETE)$/.test('GET'); // trueNamed groups (?<name>...)
Capture by name instead of index — much more readable for complex patterns.
const m = '2025-03-15'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year; // '2025'
m.groups.month; // '03'
m.groups.day; // '15'Backreferences \1, \2, …
Match the same text as a previously captured group.
/(\w+) \1/.test('hello hello'); // true — repeated word
/(\w+) \1/.test('hello world'); // false
// With named groups
/(?<word>\w+) \k<word>/.test('hello hello'); // trueLookahead & lookbehind
These match a position based on what comes before or after — without consuming the surrounding characters.
Positive lookahead (?=...)
The position must be followed by the pattern.
/\d+(?=px)/.exec('font-size: 16px'); // matches '16' (not '16px')
/(?=.*\d)/.test('abc123'); // true — somewhere ahead is a digitNegative lookahead (?!...)
The position must NOT be followed by the pattern.
/\d+(?!px)/.exec('100em'); // matches '100' (not followed by 'px')
/^(?!admin)/.test('admin'); // false
/^(?!admin)/.test('user'); // truePositive lookbehind (?<=...)
The position must be preceded by the pattern.
/(?<=\$)\d+/.exec('price: $42'); // matches '42'
/(?<=\.)\w+/.exec('user.example.com'); // matches 'example'Negative lookbehind (?<!...)
The position must NOT be preceded by the pattern.
/(?<!\d)\d{3}\b/.exec('codes 200 and 1500'); // matches '200' (no digit before)
/(?<!_)\b\w+/.exec('_private name'); // matches 'name'Lookarounds are the right tool when you want to match X only if it's surrounded by Y, but you don't want Y in your match.
Common QA patterns
Ready-to-paste regex for fields you'll validate again and again. Each one notes its trade-offs — there is no truly "correct" regex for things like email, only good-enough.
A pragmatic, good-enough pattern. The full RFC-5322 regex is famously enormous; this catches 99% of valid addresses without rejecting anything reasonable.
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$const email = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
email.test('jane.doe@example.com'); // true
email.test('foo@bar'); // false — TLD must be 2+ chars
email.test('not an email'); // falseURL (http/https)
^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$const url = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/;
url.test('https://qa.codes'); // true
url.test('http://www.example.com/path?q=1'); // true
url.test('not a url'); // falsePhone numbers
US (lenient — allows formatting):
^\+?1?[-. ]?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}$const us = /^\+?1?[-. ]?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}$/;
us.test('+1 (555) 123-4567'); // true
us.test('555-123-4567'); // true
us.test('5551234567'); // trueUK (lenient):
^\+?44\s?\d{2,5}\s?\d{3,8}$|^0\d{2,5}\s?\d{3,8}$const uk = /^\+?44\s?\d{2,5}\s?\d{3,8}$|^0\d{2,5}\s?\d{3,8}$/;
uk.test('+44 20 7946 0958'); // true
uk.test('020 7946 0958'); // trueInternational (E.164):
^\+[1-9]\d{1,14}$const e164 = /^\+[1-9]\d{1,14}$/;
e164.test('+14155551234'); // true
e164.test('+442079460958'); // trueDate formats
ISO 8601 date (YYYY-MM-DD):
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ISO 8601 timestamp:
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
iso.test('2025-03-15T14:30:00Z'); // true
iso.test('2025-03-15T14:30:00.123+05:30'); // trueUS (MM/DD/YYYY):
^(0[1-9]|1[0-2])\/(0[1-9]|[12]\d|3[01])\/\d{4}$UK (DD/MM/YYYY):
^(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/\d{4}$These do calendar shape but not validity — they accept 02/30/2025. For real validation, parse with Date or a date library after the regex passes.
IPv4 address
^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$const ipv4 = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
ipv4.test('192.168.1.1'); // true
ipv4.test('255.255.255.255'); // true
ipv4.test('256.1.1.1'); // false — out of rangeUUID v4
^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$const uuid4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
uuid4.test('550e8400-e29b-41d4-a716-446655440000'); // true
uuid4.test('not-a-uuid'); // falseThe version digit (4) and the variant bits (8, 9, a, or b) are checked, not just the shape.
Semantic versioning
The official pattern from semver.org, trimmed:
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
semver.test('1.2.3'); // true
semver.test('1.0.0-alpha.1'); // true
semver.test('1.0.0-rc.1+build.123'); // true
semver.test('1.0'); // false — must have three partsCredit card number (format only)
Shape check only — does not validate the Luhn checksum. Use a library or write the Luhn check separately.
^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$const cc = /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/;
cc.test('4111 1111 1111 1111'); // true
cc.test('4111-1111-1111-1111'); // true
cc.test('4111111111111111'); // truePassword strength
8+ chars, with at least one uppercase, lowercase, digit, and special character:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/;
strong.test('Secret!23'); // true
strong.test('secret!23'); // false — no uppercase
strong.test('Secret123'); // false — no special char
strong.test('Sec!2'); // false — too shortThe (?=...) lookaheads check each requirement independently of the others — much cleaner than trying to enforce four conditions in one linear pattern.
Testing regex in Cypress
cy.contains and most assertions accept regex directly.
// Find an element by text matching a regex
cy.contains(/Order #\d+/);
cy.contains('h2', /^Welcome/i);
// Assert the matched element's text
cy.get('[data-testid="order-id"]').should('match', /^ORD-\d{6}$/);
// Assert URL matches a pattern
cy.url().should('match', /\/orders\/\d+$/);
// Assert text content
cy.get('.timestamp').invoke('text').should('match', /^\d{4}-\d{2}-\d{2}/);
// Match against the value of an input
cy.get('input[name="orderId"]').invoke('val').should('match', /^ORD-\d+$/);
// Negative match — should NOT match
cy.get('h1').invoke('text').should('not.match', /error/i);For inputs that should match a known regex pattern, store the pattern as a constant — keeps tests readable when the pattern is non-trivial.
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
cy.get('[data-testid="user-id"]')
.invoke('text')
.should('match', UUID_RE);Testing regex in Playwright
Playwright's locator and expect APIs accept RegExp instances anywhere a string is accepted.
import { test, expect } from '@playwright/test';
test('order id matches expected shape', async ({ page }) => {
// Locator by regex text
await page.getByText(/Order #\d+/).click();
await page.getByRole('heading', { name: /^Welcome/i }).waitFor();
// Assert text matches a pattern
await expect(page.getByTestId('order-id')).toHaveText(/^ORD-\d{6}$/);
await expect(page.getByRole('alert')).toContainText(/invalid/i);
// URL match
await expect(page).toHaveURL(/\/orders\/\d+$/);
// Title match
await expect(page).toHaveTitle(/qa\.codes$/);
// Attribute match
await expect(page.getByRole('img')).toHaveAttribute('src', /\/avatars\/.+\.png$/);
// Input value match
await expect(page.getByLabel('Order ID')).toHaveValue(/^ORD-\d+$/);
});For repeat use across a suite, declare patterns once and import them — same pattern as Cypress:
// fixtures/patterns.ts
export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export const ORDER_ID_RE = /^ORD-\d{6}$/;
// in a test
import { ORDER_ID_RE } from '../fixtures/patterns';
await expect(page.getByTestId('order-id')).toHaveText(ORDER_ID_RE);A small library of named patterns makes intent obvious and lets you fix all related tests when a pattern changes.