Resource library

QA How-To

Cypress cy.task: Examples

Copy each cypress cy.task example for logging, files, downloads, CSV reports, PostgreSQL setup, parallel HTTP checks, cleanup, and safe process execution.

20 min read | 3,027 words

TL;DR

A complete Cypress task example registers a handler in setupNodeEvents and calls the same event name from a spec. Use the included recipes for logging, optional files, downloads, CSV, PostgreSQL, parallel HTTP, state, external processes, email, and cleanup, while keeping every capability narrow and bounded.

Key Takeaways

  • Every example needs a registered Node handler and a matching cy.task call in the spec.
  • Return null from logging handlers and focused plain objects from data handlers.
  • Constrain file paths, URLs, SQL operations, and child-process arguments.
  • Use unique run IDs and idempotent cleanup for parallel database tests.
  • Put deadlines on polling, HTTP, database, and process operations.
  • Return summaries instead of transferring large binaries or driver objects to the browser.

A useful cypress cy.task example has two runnable halves: a Node handler registered in setupNodeEvents, and a spec call that consumes the handler's result. The handler must finish and return JSON-serializable data, a promise that resolves to serializable data, or null. It must never fall through to undefined.

This cookbook moves from a terminal logger to optional files, download checks, database setup, parallel server calls, cross-spec state, and safe child processes. Each recipe keeps the Node capability narrow so a convenient test helper does not become an unrestricted CI back door.

Every snippet shows the Cypress-facing call and the Node-facing behavior it depends on. Replace tutorial paths, service addresses, schemas, and credentials with controlled test-environment values, then run the smallest case before composing it into a longer browser journey.

TL;DR

Example Task input Task result Better built-in when applicable
Terminal log String null cy.log() for browser Command Log only
Optional file Relative path String or null cy.readFile() when file must exist
Download metadata Filename Plain metadata object None when Node inspection is required
Database seed Domain object Created ID and safe fields Setup API through cy.request()
Parallel HTTP reads URL list Array of summarized results cy.request() for one browser-side flow
Child process Allowlisted command input Exit code and safe output cy.exec() for a direct shell command

Copy only the recipe that matches your boundary. Keep handlers in configuration or imported Node modules, while calls remain in Cypress specs.

1. Minimal Cypress cy.task Example for Terminal Logging

The smallest correct task prints a message in the terminal that launched Cypress. It explicitly returns null, because console.log() itself returns undefined.

// cypress.config.ts
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        logInfo(message: string) {
          console.log(`[Cypress] ${message}`)
          return null
        },
      })
      return config
    },
  },
})
// cypress/e2e/logging.cy.ts
it('records the order under test in CI output', () => {
  const orderId = 'ord-1042'
  cy.task('logInfo', `Verifying ${orderId}`)
  cy.visit(`/orders/${orderId}`)
  cy.contains('h1', `Order ${orderId}`).should('be.visible')
})

Use cy.log() when the message belongs in the browser Command Log. Use a logging task when CI terminal output is the important destination or when Node-side infrastructure needs to record context. Never log access tokens, passwords, session cookies, or full personally identifiable records.

For a large suite, accept a structured object with an allowlisted level and safe fields. Do not create a generic logger that serializes any object, because request clients and database results often carry secrets.

Give the message a stable run ID or business identifier when it helps correlation. Keep that identifier synthetic and non-sensitive. Terminal output from parallel workers can interleave, so a prefix such as the spec name, worker ID, or test-run key makes one line traceable without printing the full test context. Logging should support diagnosis, not become an assertion channel that a test later scrapes.

2. Read an Optional File Without Failing

cy.readFile() is the right command when a file must exist. When absence is a valid state, a Node task can return null. This example confines reads to the project's artifact directory.

// cypress/tasks/fileTasks.ts
import path from 'node:path'
import { readFile } from 'node:fs/promises'

const artifactRoot = path.resolve('cypress/artifacts')

export async function readArtifactMaybe(name: string): Promise<string | null> {
  const file = path.resolve(artifactRoot, name)
  if (!file.startsWith(`${artifactRoot}${path.sep}`)) {
    throw new Error('Artifact path is outside the allowed directory')
  }

  try {
    return await readFile(file, 'utf8')
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
    throw error
  }
}

Register the function under a descriptive event name:

on('task', {
  'artifacts:readMaybe': readArtifactMaybe,
})
cy.task<string | null>('artifacts:readMaybe', 'previous-run.txt').then((text) => {
  if (text === null) {
    cy.log('No previous-run artifact was produced')
  } else {
    expect(text).to.include('runId=')
  }
})

The handler distinguishes an expected missing file from permission, encoding, and I/O failures. Catching every exception and returning null would hide real infrastructure defects.

Validate name before resolving it. Limit length, reject null bytes, and allow only the extensions the scenario owns. The root check protects traversal, while input validation produces a clearer failure for malformed names. Decide whether symbolic links are permitted in the artifact directory, because a path can appear to be inside the root while resolving through a link to another location.

Return a discriminated object instead of string | null when the caller must distinguish several acceptable states such as missing, empty, and present. Keep the contract as simple as the scenario allows.

3. Inspect a Download With Node Metadata

A browser assertion can prove that clicking initiated a download, but Node is convenient for inspecting the resulting file. This task returns only metadata and a short signature, avoiding a large binary transfer into the spec.

import path from 'node:path'
import { open, stat } from 'node:fs/promises'

const downloadRoot = path.resolve('cypress/downloads')

async function inspectDownload(name: string) {
  const file = path.resolve(downloadRoot, name)
  if (!file.startsWith(`${downloadRoot}${path.sep}`)) {
    throw new Error('Invalid download path')
  }

  const details = await stat(file)
  const handle = await open(file, 'r')
  const buffer = Buffer.alloc(5)
  try {
    await handle.read(buffer, 0, buffer.length, 0)
  } finally {
    await handle.close()
  }

  return {
    bytes: details.size,
    signature: buffer.toString('ascii'),
  }
}
on('task', { 'downloads:inspect': inspectDownload })
cy.contains('button', 'Download PDF').click()
cy.task<{ bytes: number; signature: string }>(
  'downloads:inspect',
  'invoice-inv-42.pdf',
).then(({ bytes, signature }) => {
  expect(bytes).to.be.greaterThan(5)
  expect(signature).to.eq('%PDF-')
})

File size and signature prove transport format, not document correctness. Use an appropriate PDF parser when text, fields, accessibility, or pages matter. The Cypress file download testing guide explains how to divide browser, transport, and content coverage.

Before inspection, ensure the download finished. A file can exist while the browser is still writing it. Use the application's completed-download behavior, a deterministic filename, or a bounded stable-size check in Node. Clear the downloads directory or generate a unique name before the test so an old valid invoice cannot satisfy the task.

When the artifact can be large, read only the header required for the signature. Transferring a Buffer or full binary back through cy.task() adds serialization cost and rarely improves confidence.

4. Parse a CSV and Return Business Facts

Returning a whole report makes the spec responsible for parsing. A stronger task performs the Node-only work and returns focused facts. This example uses the maintained csv-parse package rather than an unreliable string split.

npm install --save-dev csv-parse
import path from 'node:path'
import { readFile } from 'node:fs/promises'
import { parse } from 'csv-parse/sync'

type InvoiceRow = {
  invoice_id: string
  status: string
  total: string
}

async function summarizeInvoiceCsv(name: string) {
  const file = path.resolve('cypress/downloads', name)
  const csv = await readFile(file, 'utf8')
  const rows = parse(csv, {
    columns: true,
    skip_empty_lines: true,
  }) as InvoiceRow[]

  return {
    rowCount: rows.length,
    invoiceIds: rows.map((row) => row.invoice_id),
    failedIds: rows
      .filter((row) => row.status === 'FAILED')
      .map((row) => row.invoice_id),
  }
}
on('task', { 'reports:summarizeInvoices': summarizeInvoiceCsv })
cy.task<{
  rowCount: number
  invoiceIds: string[]
  failedIds: string[]
}>('reports:summarizeInvoices', 'invoices.csv').then((summary) => {
  expect(summary.rowCount).to.eq(3)
  expect(summary.invoiceIds).to.include('inv-42')
  expect(summary.failedIds).to.deep.equal([])
})

Validate headers and numeric formats if they are part of the export contract. Return only safe data, especially when reports can contain customer information.

CSV edge cases deserve explicit fixtures. Include a quoted comma, an escaped quote, an empty optional cell, a newline inside a quoted field when supported, and the chosen line ending. Confirm whether the producer includes a byte order mark. A mature parser handles syntax, but the task must still validate required column names before calculating business facts.

Keep money as integer minor units or validated decimal strings rather than converting blindly to binary floating point. Return normalized safe values to the spec and leave raw personal columns inside Node.

5. Cypress cy.task Example for PostgreSQL Seeding

Database access belongs in Node, but exposing arbitrary SQL to specs is dangerous. This PostgreSQL recipe provides one parameterized domain operation and closes its connection through a pool owned by the plugin process.

npm install --save-dev pg @types/pg
// cypress/tasks/orderTasks.ts
import { Pool } from 'pg'

const pool = new Pool({
  connectionString: process.env.TEST_DATABASE_URL,
  max: 4,
})

type SeedOrderInput = {
  runId: string
  accountId: string
  totalCents: number
}

export async function seedOrder(input: SeedOrderInput) {
  if (!/^[a-z0-9-]{6,80}$/.test(input.runId)) {
    throw new Error('Invalid test run ID')
  }
  if (!Number.isInteger(input.totalCents) || input.totalCents < 0) {
    throw new Error('Invalid order total')
  }

  const result = await pool.query<{
    id: string
    status: string
  }>(
    `INSERT INTO test_orders (run_id, account_id, total_cents, status)
     VALUES ($1, $2, $3, 'DRAFT')
     RETURNING id, status`,
    [input.runId, input.accountId, input.totalCents],
  )

  return result.rows[0]
}
on('task', { 'db:seedOrder': seedOrder })
cy.task<{ id: string; status: string }>('db:seedOrder', {
  runId: `run-${Cypress._.random(100000, 999999)}`,
  accountId: 'acct-42',
  totalCents: 12500,
}).then((order) => {
  expect(order.status).to.eq('DRAFT')
  cy.visit(`/orders/${order.id}`)
  cy.contains('$125.00').should('be.visible')
})

Adapt table and column names to the real schema. Use a least-privileged test database, unique run IDs, and an idempotent cleanup task. Prefer Cypress API setup patterns when the service already offers an approved setup endpoint.

Manage pool lifecycle deliberately. Reuse a small pool within the Cypress plugin process rather than opening a connection for every assertion, and close it from the appropriate run-level Node event when the process requires explicit cleanup. Put a statement timeout on test queries and surface a safe operation name when it expires.

Seed the minimum state needed by the browser scenario. Bypassing application validation through direct inserts is acceptable only when the test intentionally arranges trusted fixtures and separate coverage protects the service contract.

6. Perform Parallel Server Calls in One Task

Cypress commands execute through their command queue. When several independent server-side reads must happen concurrently, a task can use Promise.all. This recipe validates URL origins before fetching.

type FetchSummary = {
  url: string
  status: number
  ok: boolean
}

const allowedOrigin = 'http://127.0.0.1:4000'

async function fetchSummaries(paths: string[]): Promise<FetchSummary[]> {
  if (!Array.isArray(paths) || paths.length === 0 || paths.length > 10) {
    throw new Error('Expected 1 to 10 service paths')
  }

  return Promise.all(paths.map(async (servicePath) => {
    const url = new URL(servicePath, allowedOrigin)
    if (url.origin !== allowedOrigin) throw new Error('Origin is not allowed')

    const response = await fetch(url, {
      signal: AbortSignal.timeout(3_000),
    })
    return { url: url.href, status: response.status, ok: response.ok }
  }))
}
on('task', { 'services:health': fetchSummaries })
cy.task<FetchSummary[]>('services:health', [
  '/catalog/health',
  '/orders/health',
  '/payments/health',
], { timeout: 8_000 }).then((results) => {
  expect(results).to.have.length(3)
  expect(results.every((result) => result.ok)).to.eq(true)
})

Limit concurrency and total inputs to avoid creating a load-test utility inside functional tests. A failed Promise.all rejects the task immediately. If all outcomes are needed, catch each request and return a deliberate result union instead of silently converting failures to success.

Choose concurrency because operations are independent, not simply because parallel looks faster. If one health endpoint depends on another service becoming ready, preserve the required order. Apply an overall deadline as well as each fetch timeout, and include the safe service name in a failed result.

Do not return full response bodies or headers from infrastructure checks. Status, duration category, and an allowlisted version or readiness field are usually enough for the spec to decide whether its prerequisite is available.

7. Keep Controlled State Between Spec Files

The Node plugin process can hold state longer than one browser spec. This can help coordinate a value produced once per run, but it is local to that Cypress process and must not be treated as globally shared across parallel CI machines.

type RunState = {
  tenantId?: string
}

const runState: RunState = {}

on('task', {
  'state:setTenant'(tenantId: string) {
    runState.tenantId = tenantId
    return null
  },

  'state:getTenant'() {
    return runState.tenantId ?? null
  },
})
// first spec
cy.request<{ tenantId: string }>('POST', '/test-support/tenants').then(({ body }) => {
  cy.task('state:setTenant', body.tenantId)
})

// later spec in the same Cypress process
cy.task<string | null>('state:getTenant').then((tenantId) => {
  expect(tenantId, 'tenant from run setup').to.be.a('string')
  cy.visit(`/tenants/${tenantId}`)
})

Cross-spec coupling makes isolated reruns harder. Prefer before setup within a spec, a run-level pipeline fixture, or a stable setup API. Use process state only when the execution contract is explicit and a missing value produces an actionable error. Never assume separate Cypress machines share the same in-memory object.

Reset process state at the beginning of a run and avoid storing credentials, large documents, or mutable domain objects. Treat stored values as a cache of small identifiers, not a source of truth. If a later spec must survive process restart or parallel distribution, put the state in an external test-support store keyed by the run rather than extending the in-memory task.

8. Run an Allowlisted External Process

cy.exec() is simpler for a direct shell command. A task is useful when arguments must be validated, execFile must avoid a shell, or output must be reshaped. This example runs only a repository-owned validation script.

import path from 'node:path'
import { promisify } from 'node:util'
import { execFile } from 'node:child_process'

const execFileAsync = promisify(execFile)
const validator = path.resolve('scripts/validate-export.mjs')

async function validateExport(name: string) {
  if (!/^[a-zA-Z0-9._-]+\.json$/.test(name)) {
    throw new Error('Invalid export filename')
  }

  const { stdout, stderr } = await execFileAsync(
    process.execPath,
    [validator, path.resolve('cypress/downloads', name)],
    { timeout: 10_000, maxBuffer: 256_000 },
  )

  return {
    output: stdout.trim(),
    warnings: stderr.trim(),
  }
}
on('task', { 'exports:validate': validateExport })
cy.task<{ output: string; warnings: string }>(
  'exports:validate',
  'account-export.json',
).then((result) => {
  expect(result.output).to.eq('VALID')
  expect(result.warnings).to.eq('')
})

execFile receives an executable and argument array without invoking a shell by default. Do not replace it with a task that accepts arbitrary command text. Cap duration and output so a faulty process cannot hang or exhaust the runner.

Check the child result contract, not only exit code. Some tools print warnings to stderr and still succeed, while others use structured JSON on stdout. Decide which behavior is acceptable and parse it inside Node. Set a controlled working directory and environment instead of inheriting every CI variable when the script does not need them.

On timeout, ensure the child is terminated and report the validator name plus safe filename. Never include a secret argument in thrown command text.

9. Poll an Email Test Service With a Deadline

Email and background-job checks are eventually consistent. Put polling behind a domain task only when holding the Cypress command is acceptable. The handler below has a fixed interval, deadline, allowed origin, and a clear timeout error.

type EmailSummary = {
  id: string
  subject: string
  recipient: string
}

const delay = (milliseconds: number) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds))

async function waitForEmail(input: {
  recipient: string
  subject: string
}): Promise<EmailSummary> {
  const deadline = Date.now() + 10_000

  while (Date.now() < deadline) {
    const query = new URLSearchParams({ recipient: input.recipient })
    const response = await fetch(
      `http://127.0.0.1:8025/api/messages?${query}`,
      { signal: AbortSignal.timeout(2_000) },
    )
    if (!response.ok) throw new Error(`Mail service returned ${response.status}`)

    const messages = await response.json() as EmailSummary[]
    const match = messages.find((message) => message.subject === input.subject)
    if (match) return match
    await delay(500)
  }

  throw new Error(`Email was not received for ${input.recipient}`)
}
on('task', { 'mail:waitFor': waitForEmail })
cy.task<EmailSummary>('mail:waitFor', {
  recipient: 'run-1042@example.test',
  subject: 'Verify your QAJobFit account',
}, { timeout: 15_000 }).its('subject').should('eq', 'Verify your QAJobFit account')

Use test-only inboxes and unique recipients. A webhook or event assertion may be better for long-running delivery flows.

Validate the recipient domain and subject length before polling so the task cannot search arbitrary mailboxes. Return a message ID and selected headers rather than the full HTML body unless content is the requirement. When content matters, parse and sanitize it in Node, then return the expected links or text fragments.

Delete test messages by run ID when the inbox service supports it. Old mail with the same subject is a common false-positive source, so compare a creation timestamp or unique correlation value as well as recipient and subject.

10. Production Cypress cy.task Example Structure

As recipes accumulate, keep configuration thin and compose named task groups. This final structure provides visible ownership and makes collisions reviewable.

// cypress/tasks/index.ts
import { readArtifactMaybe } from './fileTasks'
import { seedOrder } from './orderTasks'

export const tasks = {
  'artifacts:readMaybe': readArtifactMaybe,
  'db:seedOrder': seedOrder,
  'log:info'(message: string) {
    console.log(`[Cypress] ${message}`)
    return null
  },
}
// cypress.config.ts
import { defineConfig } from 'cypress'
import { tasks } from './cypress/tasks'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on('task', tasks)
      return config
    },
  },
})

Review every cypress cy.task example against five questions: Does it require Node? Is the input runtime-validated? Is the result small and serializable? Does it have an operation deadline? Does it expose only the minimum authority?

Add one wiring spec that calls each critical task with the smallest safe input in a controlled environment. Keep validation and dependency branches in Node unit tests. This split catches event-name and serialization mistakes without repeating dozens of cases through the browser runner.

Document required packages and environment variables beside the task module. A copied handler that compiles but lacks its driver, parser, service URL, or test credential is not operationally complete.

For broader architecture, use how to use Cypress cy.task. For synchronization after browser requests, use Cypress cy.wait with alias, because a task is not a substitute for observing the application's network behavior.

11. Idempotent Cleanup Task Example

Cleanup should be scoped to records owned by the current run and safe to execute more than once. Return a small result so the test can distinguish a successful no-op from an unexpected failure.

type CleanupResult = {
  deletedOrders: number
}

async function deleteRunData(runId: string): Promise<CleanupResult> {
  if (!/^run-[a-z0-9-]{6,80}$/.test(runId)) {
    throw new Error('Invalid cleanup run ID')
  }

  const result = await pool.query(
    'DELETE FROM test_orders WHERE run_id = $1',
    [runId],
  )
  return { deletedOrders: result.rowCount ?? 0 }
}

on('task', { 'db:deleteRunData': deleteRunData })
const runId = `run-${Cypress._.random(100000, 999999)}`

afterEach(() => {
  cy.task<CleanupResult>('db:deleteRunData', runId).then((result) => {
    expect(result.deletedOrders).to.be.at.least(0)
  })
})

The pool is the PostgreSQL pool created in the earlier recipe. A shared afterEach is appropriate only when runId is defined for every test, including setup failures. Otherwise, create the ID in beforeEach or guard the cleanup call. Do not use broad table truncation in a parallel shared environment.

If cleanup is essential for environment health, also run a scheduled janitor keyed by expired test-run IDs. Browser test teardown is best effort because a killed runner cannot execute hooks.

Record cleanup counts by resource when several tables or services are involved. An unexpected zero can reveal that setup never completed, while an unusually high count can reveal a reused run ID. Keep those diagnostics safe and do not fail merely because a correctly idempotent second cleanup deletes nothing.

Use retention limits as defense in depth. Test data should expire even when both browser hooks and the scheduled janitor encounter an outage.

12. Return Safe Failure Details From a Task

Node library errors can contain connection strings, SQL fragments, file paths, or response bodies. Translate them into an operation-specific message without turning every infrastructure failure into the same vague error.

async function inspectReportSafely(name: string) {
  try {
    return await inspectDownload(name)
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code
    if (code === 'ENOENT') {
      throw new Error(`Expected report was not found: ${name}`)
    }
    if (code === 'EACCES') {
      throw new Error(`Report could not be read: ${name}`)
    }
    throw new Error(`Report inspection failed: ${name}`, { cause: error })
  }
}

on('task', { 'reports:inspect': inspectReportSafely })

Use a safe filename or run ID in the public message, and let controlled Node logging capture a correlation ID if deeper investigation is needed. Review whether the JavaScript runtime and reporter expose cause before attaching a sensitive original error. When in doubt, keep the cause in a sanitized server-side log rather than the Cypress result.

Do not return { ok: false } for every exception and let the spec continue. Unexpected infrastructure errors should reject the task so the scenario stops at the actual failing boundary.

Preserve error categories that change the response. Missing expected output, malformed content, permission denial, dependency timeout, and programmer error should not collapse into one message. At the same time, expose only an allowlisted code and a safe operation label to the browser side.

Test sanitization with representative driver and filesystem errors. A future library upgrade can add sensitive fields even when the top-level message appears harmless.

13. Choose the Right Cypress cy.task Example

Before implementing a recipe, write the capability as a sentence: The test needs Node to parse one downloaded CSV is specific. The test needs backend access is too broad. The sentence should reveal the allowed input, resource, operation, and result.

Prototype the core work as a plain Node function. Call it with representative valid input, a boundary value, malformed input, a dependency failure, and a timeout. Then register a thin adapter under a namespaced event and add one Cypress integration check. This separates logic defects from task wiring defects.

Measure the cost in CI. If the task runs once per test but produces identical setup, move it to per-spec setup or pipeline provisioning. If it transfers megabytes only for two assertions, return a summary. If several testing tools need the same capability, an authenticated test-support service may be a better long-term interface than a Cypress-specific task.

Finally, document cleanup and parallel behavior. State whether resources are scoped by run ID, whether memory is process-local, and whether repeating the operation is safe. These constraints matter more to reliability than clever task syntax.

Interview Questions and Answers

Q: Show the minimum valid task handler.

I register an object with on('task', ...) inside setupNodeEvents. A handler such as log(message) { console.log(message); return null } is valid because it ends and explicitly returns null. The spec calls it with cy.task('log', message).

Q: How would you read a file that might not exist?

I create a path-constrained Node task that attempts readFile. It returns null only for the ENOENT case and rethrows permission or I/O errors. If the file must exist, I use cy.readFile() instead.

Q: How do you pass several values to a task?

I pass one plain object, such as { runId, accountId, sku }, then validate it at runtime in Node. Task arguments cross a JSON serialization boundary, so I avoid functions and class instances.

Q: How would you seed a database safely?

I expose a domain handler such as db:seedOrder, use parameterized queries in a repository, and read credentials from the Node environment. The handler returns only safe identifiers and fields. I never accept raw SQL from the spec.

Q: When is process state between specs unsafe?

It is unsafe whenever tests run in separate Cypress processes or CI machines, because memory is not shared. It also creates order dependence and makes isolated reruns harder. I prefer external run fixtures or setup APIs unless the single-process contract is explicit.

Q: How do you execute a child process safely?

I prefer cy.exec() for a simple command. For a task wrapper, I use execFile with a fixed executable, validate each argument, avoid a shell, and cap time and output. I never accept arbitrary command text.

Q: How do you test eventual email delivery with a task?

I poll a test-only inbox with a unique recipient and a strict deadline. Each HTTP call has its own timeout, terminal service errors fail immediately, and the task timeout is slightly larger than the internal deadline. Long waits belong in event-based infrastructure rather than a browser suite.

Common Mistakes

  • Returning the result of console.log(), which is undefined, instead of returning null.
  • Reading arbitrary paths supplied by a spec without resolving them under an allowed root.
  • Catching every optional-file error and hiding permissions or disk failures.
  • Sending an entire binary or database response across the task boundary.
  • Parsing CSV by splitting on commas, which breaks quoted fields.
  • Accepting raw SQL or interpolating values instead of parameterizing them.
  • Assuming in-memory task state is shared across parallel CI machines.
  • Executing arbitrary shell strings or unvalidated URLs.
  • Polling without both request-level timeouts and an overall deadline.
  • Using tasks for ordinary browser traffic that cy.intercept() should observe.

Conclusion

The strongest cypress cy.task example is small, explicit, and privileged only as far as the scenario requires. Register a named Node handler, validate one serializable input, return a focused result or null, and give the operation a deadline. The recipes above cover logging, optional files, downloads, reports, databases, concurrent requests, process state, child processes, and eventual email without inventing Cypress APIs.

Begin with the minimal logger to confirm configuration, then implement one domain task around a real Node-only need. Keep the core logic in a plain module, unit test it where valuable, and review the handler as carefully as application infrastructure.

Interview Questions and Answers

How would you implement terminal logging with cy.task?

I register a handler that accepts a safe string, writes it with console.log, and returns null. The spec calls that event with cy.task. I keep secrets and large objects out of both the argument and terminal output.

How do you prevent a file task from reading arbitrary host files?

I resolve the candidate under a fixed artifact root and verify the resolved path starts with the root plus the platform separator. I also validate allowed extensions and size where appropriate. Path restriction occurs in Node, not only through TypeScript types.

What makes a database seed task parallel-safe?

Each worker uses a unique run ID and creates records owned by that namespace. Queries are parameterized and cleanup deletes only records for that run. Shared fixed emails, IDs, or table truncation would create collisions.

When would you use Promise.all inside a task?

I use it for a small bounded set of independent server-side operations that truly benefit from concurrency. I validate allowed origins and cap list size and request duration. I avoid turning a functional task into an uncontrolled load generator.

How do you poll an email service from a task?

I use a unique test recipient, an allowlisted test-service origin, per-request timeouts, and an overall deadline. A matching message ends the task, terminal service failures reject immediately, and deadline expiry produces a specific error. Long delivery workflows should use event-based infrastructure.

How do you safely report task failures?

I translate expected dependency errors into operation-specific messages with safe filenames or run IDs. I avoid dumping original objects that may contain credentials or private records. Unexpected failures reject the task so the scenario stops at the real boundary.

How do you decide whether an example belongs in cy.task?

I state the required Node capability and check whether cy.readFile, cy.request, cy.writeFile, cy.exec, or cy.intercept already represents it. If Node is essential, I define the smallest allowed input and result. I also document timeout, parallel, and cleanup behavior before reuse.

Frequently Asked Questions

What is the simplest Cypress cy.task example?

Register a log handler that prints one string and returns null, then call cy.task('logInfo', message) from the spec. Returning null is important because console.log returns undefined, which Cypress rejects as an unhandled result.

How can a task read a file that might not exist?

Resolve the supplied name under an allowed artifact directory and catch only the ENOENT error. Return null for that expected absence and rethrow permissions or I/O failures. Use cy.readFile instead when the file is required.

Can Cypress use cy.task with PostgreSQL?

Yes. Register a Node handler that uses a PostgreSQL client such as pg and parameterized queries. Expose a domain operation like db:seedOrder instead of accepting arbitrary SQL, and keep the connection string in the Node environment.

How do I parse a downloaded CSV in a Cypress task?

Read the file from a constrained downloads directory and parse it with a CSV library that supports quoted fields. Return focused facts such as row count and relevant IDs. Avoid sending the entire report into the browser when only a few assertions are needed.

Can a Cypress task share data between spec files?

A task handler can keep state in its Node process, but that state is not shared across parallel Cypress processes or CI machines. It also introduces order coupling. Prefer setup APIs or external run fixtures unless the single-process contract is explicit.

How can a task run an external script safely?

Use execFile with a fixed executable and repository-owned script, validate each argument, avoid a shell, and cap timeout and output. For a direct uncomplicated command, cy.exec may be clearer. Never expose a task that accepts arbitrary command text.

How should a cleanup task be designed?

Scope cleanup to a unique run ID, validate that ID, use parameterized operations, and make repeated calls safe. Return a small count or status for diagnostics. Add an environment janitor because a terminated browser process cannot guarantee teardown hooks.

Related Guides