Skip to main content

Tag System

Tags organize tests and control which steps and scenarios execute.

Overview

Project Tags

Project tags determine which test suite a scenario belongs to:

TagDescriptionSteps Available
@apiHTTP API testsAPI auth, HTTP, assertions
@uiBrowser UI testsNavigation, forms, assertions
@tuiTerminal UI testsTUI input, output, snapshots
@hybridCross-layer testsAll step types

Usage

@api
Feature: User API
# All scenarios use API steps

@ui
Feature: Login Page
# All scenarios use UI steps

@tui
Feature: CLI Application
# All scenarios use TUI steps

@hybrid
Feature: End-to-End Flow
# Scenarios can mix API and UI steps

Modifier Tags

Modifier tags add metadata or control execution:

Built-in Modifiers

TagDescription
@SkipSkip this scenario
@ignoreSkip this scenario (alias)
@wipWork in progress
@smokeQuick smoke test
@slowLong-running test
@criticalCritical path test
@externalRequires external service

Custom Modifiers

Define your own:

@api @regression
Scenario: Complex user workflow
...

@ui @visual
Scenario: Check page layout
...

Tag Expressions

Tag expressions filter which tests run.

Operators

OperatorExampleDescription
and@api and @smokeBoth tags required
or@smoke or @criticalEither tag
notnot @SkipTag absent
()(@smoke or @critical)Grouping

Examples

# Run only API smoke tests
--grep "@api and @smoke"

# Run smoke or critical tests
--grep "@smoke or @critical"

# Exclude slow tests
--grep "not @slow"

# Complex expression
--grep "@api and (@smoke or @critical) and not @external"

Configuration Helpers

tagsForProject

Builds tag expressions with default excludes:

import { tagsForProject } from '@esimplicity/stack-tests';

// Basic - adds default excludes
tagsForProject({ projectTag: '@api' })
// Result: "not @Skip and not @ignore and @api"

// With extra tags
tagsForProject({ projectTag: '@api', extraTags: '@smoke' })
// Result: "not @Skip and not @ignore and @api and (@smoke)"

// Custom excludes
tagsForProject({
projectTag: '@api',
defaultExcludes: 'not @Skip and not @wip'
})
// Result: "not @Skip and not @wip and @api"

resolveExtraTags

Normalizes tag input from environment or CLI:

import { resolveExtraTags } from '@esimplicity/stack-tests';

// Tag expression (passed through)
resolveExtraTags('@smoke or @critical')
// Result: "@smoke or @critical"

// Comma-separated (converted to OR)
resolveExtraTags('smoke,critical')
// Result: "@smoke or @critical"

// Single tag
resolveExtraTags('smoke')
// Result: "@smoke"

// Empty/null
resolveExtraTags('')
// Result: undefined

Project Configuration

Playwright Config

// playwright.config.ts
import { defineBddProject } from 'playwright-bdd';
import { tagsForProject, resolveExtraTags } from '@esimplicity/stack-tests';

const extraTags = resolveExtraTags(process.env.TEST_TAGS);

const apiBdd = defineBddProject({
name: 'api',
features: 'features/api/**/*.feature',
steps: 'features/steps/**/*.ts',
tags: tagsForProject({ projectTag: '@api', extraTags }),
});

const uiBdd = defineBddProject({
name: 'ui',
features: 'features/ui/**/*.feature',
steps: 'features/steps/**/*.ts',
tags: tagsForProject({ projectTag: '@ui', extraTags }),
});

Running with Tags

# Via environment variable
TEST_TAGS=@smoke npm test

# Via Playwright grep
npx playwright test --grep "@smoke"

# Specific project
npx playwright test --project=api

# Combined
TEST_TAGS=@critical npx playwright test --project=api

Step-Level Tags

Steps are tagged to ensure they run in appropriate contexts. Steps use tag expressions to support multiple scenario types:

// Available in @api and @hybrid scenarios
When('I GET {string}', { tags: '@api or @hybrid' }, async ({ api }, path) => {
// ...
});

// Available in @ui and @hybrid scenarios
When('I click the button {string}', { tags: '@ui or @hybrid' }, async ({ ui }, name) => {
// ...
});

// Available in @tui scenarios only
When('I type {string}', { tags: '@tui' }, async ({ tui }, text) => {
// ...
});

// Available everywhere (no tag)
Given('I set variable {string} to {string}', async ({ world }, name, value) => {
// ...
});

This pattern allows @hybrid scenarios to use both API and UI steps in the same test.

Tag Inheritance

Feature-Level Tags

Tags on the feature apply to all scenarios:

@api @smoke
Feature: Quick API Tests
# Both @api and @smoke apply to all scenarios

Scenario: Health check
# Inherits @api and @smoke
When I GET "/health"

Scenario: Version check
# Inherits @api and @smoke
When I GET "/version"

Scenario-Level Tags

Tags on scenarios add to (not replace) feature tags:

@api
Feature: User API

@smoke
Scenario: Quick health check
# Tags: @api, @smoke
When I GET "/health"

@slow @external
Scenario: Integration test
# Tags: @api, @slow, @external
When I POST "/external-sync" ...

Rule-Level Tags

Tags on Rules apply to scenarios within:

@api
Feature: User API

@admin
Rule: Admin operations

Scenario: Create user
# Tags: @api, @admin
...

Scenario: Delete user
# Tags: @api, @admin
...

@member
Rule: Member operations

Scenario: View profile
# Tags: @api, @member
...

Common Patterns

Smoke Test Suite

# features/api/health.feature
@api @smoke
Feature: API Health Checks

Scenario: Health endpoint
When I GET "/health"
Then the response status should be 200

# features/ui/login.feature
@ui @smoke
Feature: Login Smoke Test

Scenario: Page loads
Given I navigate to "/login"
Then I should see text "Sign In"
# Run all smoke tests
TEST_TAGS=@smoke npm test

Skip Work in Progress

@api
Feature: New Feature

@wip
Scenario: Not ready yet
# Skipped by default tag expression
...

Scenario: Working test
# Runs normally
...

External Dependencies

@api
Feature: External Integration

@external
Scenario: Call third-party API
# Tag allows filtering when service unavailable
When I POST "/external-webhook" ...
# Skip external tests
npx playwright test --grep "not @external"

Managing Work-in-Progress Features

When writing features incrementally, you may have scenarios with missing step definitions. Instead of blocking all test generation, use tags to exclude incomplete work:

Pattern 1: Use @wip to exclude incomplete features

@api @wip
Feature: Payment Processing
# Step definitions not yet implemented

Scenario: Process credit card payment
Given the payment gateway is configured
When I process payment for "order-123"
Then the transaction should be recorded

Configure your project to exclude @wip:

// playwright.config.ts
const apiBdd = defineBddProject({
name: 'api',
features: 'features/api/**/*.feature',
steps: 'features/steps/**/*.ts',
tags: '@api and not @wip', // Exclude work-in-progress
});

Pattern 2: Use @ready to include only complete features

@api @ready
Feature: User Management
# All step definitions implemented

Scenario: Create a new user
When I POST "/users" with JSON body:
"""
{ "email": "test@example.com" }
"""
Then the response status should be 201
// playwright.config.ts
const apiBdd = defineBddProject({
name: 'api',
features: 'features/api/**/*.feature',
steps: 'features/steps/**/*.ts',
tags: '@api and @ready', // Only generate ready features
});

Workflow:

  1. Write feature file with @wip tag (or without @ready)
  2. Run npm run gen:stubs to generate step stubs
  3. Implement step definitions
  4. Remove @wip (or add @ready) when complete
  5. Run npm run gen and npm test

This approach lets you develop features incrementally while keeping your test suite runnable.

Best Practices

Consistent Tagging

# Good - clear categorization
@api @smoke @auth
Scenario: Login returns token

# Avoid - inconsistent
@API @Smoke @AUTH # Case inconsistency

Minimal Tags

# Good - necessary tags only
@api @critical
Scenario: Payment processing

# Avoid - over-tagging
@api @critical @payment @backend @integration @v2
Scenario: Payment processing

Document Custom Tags

## Project Tags

| Tag | Description | Owner |
|-----|-------------|-------|
| @payments | Payment service tests | payments-team |
| @legacy | Legacy API tests | platform-team |