Test Automation 7 min read

Regex for QA Engineers: Practical Patterns

A hands-on guide to regular expressions for QA engineers. Learn the regex patterns you'll actually use in Playwright, Jest, and API testing — with real.

I
InnovateBits
InnovateBits

Regular expressions get a reputation for being cryptic and write-only. But for a QA engineer, a handful of well-understood patterns cover the vast majority of real-world use cases. You don't need to master every regex feature — you need to know the patterns for the things you actually test: emails, phone numbers, dates, IDs, URLs, and log messages.

This guide focuses on practical patterns you can use directly in your test code, with explanations of why each pattern is written the way it is.


How regex matching works in JavaScript

JavaScript's regex engine is what powers every testing framework — Playwright, Jest, Cypress, and Vitest all use the same V8 regex implementation. Two methods are most relevant in testing:

test(string) — returns true or false. Use this for assertions.

TYPESCRIPT
1const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 2emailPattern.test('alice@example.com') // true 3emailPattern.test('not-an-email') // false

match(regex) — returns the match array or null. Use this when you need to extract the matched value.

TYPESCRIPT
1const logLine = '2025-12-14T10:30:00Z [ERROR] Payment service timeout after 5000ms' 2const timestampMatch = logLine.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/) 3console.log(timestampMatch?.[0]) // "2025-12-14T10:30:00Z"

The flags you need to understand

Flags modify how the pattern engine processes the string. In test automation, these three matter most:

FlagEffectWhen to use
gGlobal — find all matches, not just the firstCounting occurrences, extracting all matching values
iCase insensitiveEmail validation, status strings, error messages
mMultiline — ^ and $ match line boundariesParsing multi-line log output
TYPESCRIPT
1// Without `i` flag — only matches "ERROR" 2/ERROR/.test('error') // false 3 4// With `i` flag — matches any case 5/ERROR/i.test('error') // true 6/ERROR/i.test('Error') // true 7/ERROR/i.test('ERROR') // true

Essential patterns for test assertions

Email address

TYPESCRIPT
1// Basic — catches most invalid emails without being overly strict 2const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 3 4// Use in Jest 5expect(user.email).toMatch(emailRegex) 6 7// Use in Playwright page assertion 8await expect(page.locator('[data-testid="user-email"]')).toHaveText(emailRegex)

Why not use a "perfect" email regex? The RFC 5322 compliant email regex is 6,000+ characters long and still misses edge cases. For testing that a field looks like an email, the simple pattern above is correct 99.9% of the time.

UUID (version 4)

TYPESCRIPT
1const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i 2 3expect(response.body.id).toMatch(uuidV4Regex)

The 4 in the third group and [89ab] in the fourth group are the distinguishing features of UUID v4. If you just need to check that something looks like any UUID format, use the looser pattern:

TYPESCRIPT
1const uuidAnyRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i

ISO 8601 date-time

TYPESCRIPT
1// Matches: 2025-12-14T10:30:00Z or 2025-12-14T10:30:00.000Z or 2025-12-14T10:30:00+05:30 2const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/ 3 4expect(order.createdAt).toMatch(isoDateTimeRegex) 5 6// Just a date (no time component) 7const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/ 8expect(event.date).toMatch(isoDateRegex)

Phone number (US format)

TYPESCRIPT
1// Matches: +1-555-123-4567, (555) 123-4567, 555.123.4567, 5551234567 2const usPhoneRegex = /^(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/ 3 4expect(user.phone).toMatch(usPhoneRegex)

For international numbers, consider whether you need to validate the format at all in your tests — often checking that the field is non-empty and contains only digits, spaces, hyphens, and parentheses is sufficient.

URL

TYPESCRIPT
1// Matches http and https URLs 2const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$/ 3 4expect(product.imageUrl).toMatch(urlRegex) 5 6// More specific — only HTTPS 7const httpsUrlRegex = /^https:\/\/.+/ 8expect(paymentRedirectUrl).toMatch(httpsUrlRegex)

Credit card number (redacted format)

APIs should never return full card numbers. Assert that they're returning the masked format:

TYPESCRIPT
1// Matches: **** **** **** 4242 or ****4242 2const maskedCardRegex = /^(\*{4}\s?){3}\d{4}$|^\*{4}\d{4}$/ 3 4expect(payment.cardNumber).toMatch(maskedCardRegex)

Regex in Playwright

Playwright's toHaveText, toContainText, and getByText all accept regex patterns directly. This is significantly more robust than exact string matching for dynamic content.

TYPESCRIPT
1// Exact text — fragile, breaks if punctuation changes 2await expect(page.locator('.status')).toHaveText('Order confirmed!') 3 4// Regex — robust, ignores surrounding whitespace and exact punctuation 5await expect(page.locator('.status')).toHaveText(/order confirmed/i)

Finding elements with dynamic IDs

TYPESCRIPT
1// Don't use exact id when it changes per-session 2await page.locator('[data-testid="order-ord_abc123"]') // fragile 3 4// Use regex to match the pattern 5await page.locator('[data-testid^="order-"]') // CSS starts-with 6await page.getByTestId(/^order-/) // regex

Asserting error messages

Error messages often include dynamic values (field names, limits, actual values). Regex lets you assert on the structure without hardcoding the dynamic part:

TYPESCRIPT
1// The exact count might vary — assert the pattern 2await expect(page.locator('.error-message')).toHaveText(/must be at least \d+ characters/) 3 4// Assert it contains the field name but not the exact wording 5await expect(page.locator('.field-error')).toHaveText(/email/i)

Regex in Jest

Jest's expect(value).toMatch(regex) works on strings. For arrays, use expect.arrayContaining with expect.stringMatching:

TYPESCRIPT
1// Assert all emails in a list match the pattern 2const users = await api.getUsers() 3const emails = users.map(u => u.email) 4expect(emails).toEqual( 5 expect.arrayContaining([ 6 expect.stringMatching(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) 7 ]) 8) 9 10// Assert at least one item in the response contains an error about "email" 11const errors = response.body.errors 12expect(errors.some(e => /email/i.test(e.field))).toBe(true)

Parsing log output with regex

Log parsing is a QA-adjacent task that comes up during CI failure investigation, performance analysis, and monitoring setup. Regex makes it tractable.

Extracting error counts from CI output

TYPESCRIPT
1const ciLog = ` 2Running 124 tests across 8 workers 3 ✓ 119 passed (45.2s) 4 ✗ 5 failed 5` 6 7const failedMatch = ciLog.match(/(\d+) failed/) 8const failedCount = failedMatch ? parseInt(failedMatch[1], 10) : 0 9console.log(`Failed tests: ${failedCount}`) // 5

Parsing structured log lines

TYPESCRIPT
1// Log format: 2025-12-14T10:30:00Z [LEVEL] Message 2const logPattern = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+\[(\w+)\]\s+(.+)$/m 3 4const logLine = '2025-12-14T10:30:00Z [ERROR] Database connection timeout' 5const match = logLine.match(logPattern) 6 7if (match) { 8 const [, timestamp, level, message] = match 9 console.log({ timestamp, level, message }) 10 // { timestamp: '2025-12-14T10:30:00Z', level: 'ERROR', message: 'Database connection timeout' } 11}

Counting specific events in a log file

TYPESCRIPT
1import { readFileSync } from 'fs' 2 3const logs = readFileSync('test-run.log', 'utf8') 4const errorLines = logs.match(/\[ERROR\]/gm) ?? [] 5const warnLines = logs.match(/\[WARN\]/gm) ?? [] 6 7console.log(`Errors: ${errorLines.length}, Warnings: ${warnLines.length}`)

Building and testing patterns

Regex patterns are easy to get wrong in subtle ways — a pattern that works for your test cases might fail on edge cases you haven't considered. Using an interactive tester while building patterns eliminates this.

The Regex Tester tool on this site shows live match highlighting as you type. Key things to verify:

  1. Boundary anchors — does your pattern use ^ and $ to prevent partial matches? Without anchors, /\d{4}/ matches in "abc1234def".
  2. Special character escaping. in regex matches any character. If you mean a literal dot, use \..
  3. Edge cases — test the empty string, very long strings, and strings containing regex special characters ([, ], (, ), *, +, ?, {, }, |, ^, $, ., \).
TYPESCRIPT
1// Common mistake: unescaped dot matches any character 2/example.com/.test('exampleXcom') // true (wrong) 3/example\.com/.test('exampleXcom') // false (correct)

Quick reference: regex syntax

PatternMeaningExample
.Any character except newline/a.c/ matches abc, aXc
\dDigit [0-9]/\d{4}/ matches 2025
\wWord char [a-zA-Z0-9_]/\w+/ matches hello_123
\sWhitespace/a\sb/ matches a b
^Start of string (or line with m)/^hello/
$End of string (or line with m)/world$/
+One or more/a+/ matches a, aa, aaa
*Zero or more/a*/ matches ``, a, aa
?Zero or one/colou?r/ matches color, colour
{n,m}Between n and m times/\d{2,4}/ matches 2–4 digits
[abc]Character class/[aeiou]/ matches any vowel
[^abc]Negated class/[^0-9]/ matches non-digits
(a|b)Alternation/(cat|dog)/ matches cat or dog
(...)Capture group/(hello) (world)/ captures both words
(?:...)Non-capture groupGroups without capturing
\bWord boundary/\btest\b/ matches test not testing
Tags
#regex#regular-expressions#test-automation#playwright#jest#qa-tools

Share this article

Follow for more

Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!