<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vanessa Sastre</title>
    <description>The latest articles on DEV Community by Vanessa Sastre (@vanessa_sastre).</description>
    <link>https://dev.to/vanessa_sastre</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4040790%2F0dce6a62-9dc7-4826-b642-08fa1e1f8868.png</url>
      <title>DEV Community: Vanessa Sastre</title>
      <link>https://dev.to/vanessa_sastre</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vanessa_sastre"/>
    <language>en</language>
    <item>
      <title>AI Security Testing 102: Sensitive Data Leak, Input Leakage, and Prompt Disclosure</title>
      <dc:creator>Vanessa Sastre</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:20:00 +0000</pubDate>
      <link>https://dev.to/vanessa_sastre/ai-security-testing-102-sensitive-data-leak-input-leakage-and-prompt-disclosure-4mnh</link>
      <guid>https://dev.to/vanessa_sastre/ai-security-testing-102-sensitive-data-leak-input-leakage-and-prompt-disclosure-4mnh</guid>
      <description>&lt;p&gt;This is the second post in a new series on AI application security testing, following the structure of the &lt;a href="https://owasp.org/www-project-ai-testing-guide/" rel="noopener noreferrer"&gt;OWASP AI Testing Guide&lt;/a&gt;'s AI Application Testing category (AITG-APP).&lt;/p&gt;

&lt;p&gt;The previous post covered prompt injection: testing whether attacker-controlled input can change an application's behavior in ways that violate its intended instructions or security constraints. This post focuses on a related but distinct question: &lt;strong&gt;whether an AI application exposes information that should remain protected.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We're covering three tests from the guide. Although all three involve information being exposed in an unintended way, they target different sources of information and different security boundaries.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;The question it asks&lt;/th&gt;
&lt;th&gt;Where the information comes from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AITG-APP-03&lt;/strong&gt; — Sensitive data leak&lt;/td&gt;
&lt;td&gt;Can the application expose confidential information through its model, memory, training data, or connected tools?&lt;/td&gt;
&lt;td&gt;Model, memory, training data, retrieved documents, connected tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AITG-APP-04&lt;/strong&gt; — Input leakage&lt;/td&gt;
&lt;td&gt;Can information supplied by a user be unnecessarily reproduced or exposed through the application?&lt;/td&gt;
&lt;td&gt;The user's own input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AITG-APP-07&lt;/strong&gt; — Prompt disclosure&lt;/td&gt;
&lt;td&gt;Can the application reveal its system or application instructions?&lt;/td&gt;
&lt;td&gt;The application's instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction matters because the same apparent symptom can have very different causes. Effective testing therefore needs to establish &lt;strong&gt;where the information came from, whether the disclosure was unauthorized, and which security control should have prevented it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sensitive Data Leak (AITG-APP-03)
&lt;/h2&gt;

&lt;p&gt;Sensitive data leakage occurs when an AI application exposes confidential information that should not be available to the requesting user or should not leave a protected system boundary.&lt;/p&gt;

&lt;p&gt;The source of the information can vary. It may come from training data, conversation memory, retrieved documents, application context, databases, or connected tools. The testing approach therefore depends on what information the application can access and which users are authorized to see it.&lt;/p&gt;

&lt;p&gt;A useful starting point is to identify what the application should protect and where that information enters the system. Testing can then examine whether the model or surrounding application exposes it through direct requests, contextual tasks, or unintended information disclosure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attack Patterns Worth Knowing
&lt;/h3&gt;

&lt;p&gt;Several approaches can help testers identify whether sensitive information is exposed through an AI application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Direct and contextual requests
&lt;/h4&gt;

&lt;p&gt;Simply asking the model for credentials or confidential information is an obvious starting point. A more contextual test might ask the model to summarize internal communications or retrieve information from a document that happens to contain sensitive values.&lt;/p&gt;

&lt;p&gt;The distinction matters because an application may refuse a direct request for a password while still exposing the same information through an apparently legitimate task, such as summarization or document analysis.&lt;/p&gt;

&lt;h4&gt;
  
  
  Persona and pretext framing
&lt;/h4&gt;

&lt;p&gt;A request might frame the interaction as a developer debugging the application, an administrator investigating an error, or a researcher examining a system's configuration.&lt;/p&gt;

&lt;p&gt;These scenarios can help determine whether the application relies too heavily on the model's interpretation of the user's claimed role or purpose. &lt;strong&gt;Authorization should be enforced by the application rather than inferred from a persuasive explanation in the prompt.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Tool and application reconnaissance
&lt;/h4&gt;

&lt;p&gt;For agentic systems, testers can examine whether the model reveals available tools, sub-agents, tool parameters, or other operational details.&lt;/p&gt;

&lt;p&gt;This information is not necessarily confidential, and revealing a tool description is not automatically a vulnerability. However, unnecessary disclosure can help an attacker understand the application's capabilities and construct more targeted follow-up attacks.&lt;/p&gt;

&lt;p&gt;Testing should establish whether the disclosed information violates a confidentiality requirement or meaningfully weakens a security boundary.&lt;/p&gt;

&lt;h4&gt;
  
  
  Implementation details and error disclosure
&lt;/h4&gt;

&lt;p&gt;Requests for stack traces, API endpoints, framework details, internal configuration, or recent errors can reveal information about the application's implementation.&lt;/p&gt;

&lt;p&gt;Individually, some of these details may be harmless. Others can expose sensitive values, internal infrastructure, or information that should be restricted to administrators. The security impact depends on what is disclosed and who can access it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Unifying Theme
&lt;/h3&gt;

&lt;p&gt;A sensitive data leak is not always a model revealing a password. Information about internal systems, retrieved records, or connected tools can also be relevant when its disclosure violates the application's intended security constraints.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Distinguish actual disclosure from speculation.&lt;/strong&gt; A model describing a hypothetical environment does not prove that it has access to the corresponding credentials, files, or infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Input Leakage (AITG-APP-04)
&lt;/h2&gt;

&lt;p&gt;Input leakage examines whether information supplied to an AI application is exposed or reused in ways that violate the application's intended privacy or security boundaries.&lt;/p&gt;

&lt;p&gt;Unlike a sensitive data leak, where the information may originate from a protected data source or connected system, this test starts with information provided through the input channel itself.&lt;/p&gt;

&lt;p&gt;A practical test might use a synthetic API key, a fake password, or a clearly marked confidential string. The tester then checks whether the application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reproduces that value in a later response,&lt;/li&gt;
&lt;li&gt;exposes it to another user or session, or&lt;/li&gt;
&lt;li&gt;passes it to a component that should not receive it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important question is not simply whether the model can repeat the value. &lt;strong&gt;It is whether the application handles that value in accordance with its security requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Test Patterns
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Repeated-input testing
&lt;/h4&gt;

&lt;p&gt;Provide a synthetic sensitive value and check whether the application reproduces it when asked directly or in a later turn.&lt;/p&gt;

&lt;p&gt;Where relevant, test session boundaries and whether information can appear in another user's context. This helps distinguish ordinary repetition within a conversation from unintended exposure across users or sessions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Normalization and obfuscation
&lt;/h4&gt;

&lt;p&gt;Represent test values using alternative character encodings or Unicode variants, then evaluate whether the application's detection and redaction controls recognize the value.&lt;/p&gt;

&lt;p&gt;For example, a test might use Unicode full-width digits instead of standard ASCII digits. A filter that relies on a narrowly defined character pattern may handle the two representations differently, while a downstream component may normalize them to the same value.&lt;/p&gt;

&lt;p&gt;This is not a guaranteed bypass. The result depends on how inputs are normalized, inspected, and processed throughout the application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Contextual variation
&lt;/h4&gt;

&lt;p&gt;Change the surrounding request while keeping the underlying sensitive value the same.&lt;/p&gt;

&lt;p&gt;For example, compare a direct request to reproduce a value with a request to summarize, transform, or explain the message containing it. The objective is to determine whether the application's protections depend too heavily on specific keywords or request formats.&lt;/p&gt;

&lt;h4&gt;
  
  
  Affix and formatting variations
&lt;/h4&gt;

&lt;p&gt;Add unrelated text immediately before or after a synthetic sensitive value, or place the value within a larger structured input.&lt;/p&gt;

&lt;p&gt;These variations can help identify filters that rely on particular positions, delimiters, or surrounding context. Their effectiveness depends on the implementation being tested.&lt;/p&gt;

&lt;h4&gt;
  
  
  Combined test cases
&lt;/h4&gt;

&lt;p&gt;Combine several input variations to evaluate whether the application's controls continue to work when multiple techniques are used together.&lt;/p&gt;

&lt;p&gt;For example, a test might combine Unicode variations with additional surrounding text and a contextual request. This can help establish whether different defensive layers work together or whether one layer depends on another recognizing a particular representation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Ground Rules
&lt;/h3&gt;

&lt;p&gt;These tests should use synthetic values rather than real credentials or personal information. They should also distinguish between the model's response, application logs, downstream tool calls, and any cross-session exposure being tested.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A clean result has limits.&lt;/strong&gt; A successful output test only establishes what happened in the observed interaction. It does not establish whether the provider retains inputs, whether those inputs are used for training, or how they are handled in application logs. Those questions require separate examination of the system's data-handling configuration and policies.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Prompt Disclosure (AITG-APP-07)
&lt;/h2&gt;

&lt;p&gt;Prompt disclosure tests whether an AI application reveals system or application instructions that are intended to remain confidential.&lt;/p&gt;

&lt;p&gt;These instructions may contain proprietary workflows, internal policy details, tool descriptions, or other implementation information. Their disclosure can help an attacker understand how the application is designed and identify additional attack paths.&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;a system prompt should not be treated as a security boundary in its own right.&lt;/strong&gt; Secrets should not be embedded in instructions, and authorization should be enforced by the application rather than relying on the model to follow a prompt.&lt;/p&gt;

&lt;p&gt;Testing should therefore establish both whether protected instructions can be reconstructed and whether their disclosure creates a meaningful security or operational impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Techniques Worth Testing
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Direct and oblique requests
&lt;/h4&gt;

&lt;p&gt;Simply asking the model to reveal its system prompt is an obvious starting point. More indirect requests might ask it to explain its guidelines, describe its operating instructions, or summarize the rules governing its behavior.&lt;/p&gt;

&lt;p&gt;Oblique phrasing can sometimes produce different results from a direct request, depending on the model and the application's safeguards. Testers should evaluate both approaches rather than assuming that blocking one phrase prevents disclosure.&lt;/p&gt;

&lt;h4&gt;
  
  
  Context and clarification framing
&lt;/h4&gt;

&lt;p&gt;A request might claim that the model's behavior is confusing and ask it to quote the instructions responsible for that behavior.&lt;/p&gt;

&lt;p&gt;This tests whether the model treats a request framed as clarification differently from an explicit request for confidential instructions. The important question is whether the application exposes information that its security requirements say should remain protected.&lt;/p&gt;

&lt;h4&gt;
  
  
  Partial completion and reconstruction
&lt;/h4&gt;

&lt;p&gt;A tester can examine whether supplying a suspected prefix of an instruction causes the model to continue with confidential text.&lt;/p&gt;

&lt;p&gt;Language models generate text based on their learned behavior and the context they receive, including instructions and learned refusal behavior. Prefix completion may therefore reveal information in some configurations, but it does not guarantee disclosure.&lt;/p&gt;

&lt;p&gt;Testing should also distinguish between the model producing a plausible continuation and actually reproducing the application's real system prompt.&lt;/p&gt;

&lt;h4&gt;
  
  
  Incremental extraction
&lt;/h4&gt;

&lt;p&gt;Instead of requesting an entire prompt at once, a tester can examine whether the application reveals protected instructions in smaller pieces.&lt;/p&gt;

&lt;p&gt;For example, successive requests might ask for individual instructions, a summary of particular rules, or additional details about previously disclosed text. The objective is to determine whether restrictions that apply to a single large request remain effective across multiple interactions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Adversarial suffixes (a related research area)
&lt;/h4&gt;

&lt;p&gt;Adversarial suffixes are specially constructed text appended to a prompt to influence a model's behavior. Some research has demonstrated that such techniques can induce undesirable outputs or weaken refusal behavior in particular configurations.&lt;/p&gt;

&lt;p&gt;However, causing a model to produce an unwanted response is not the same as demonstrating system-prompt extraction. &lt;strong&gt;A prompt-disclosure finding requires evidence that protected instructions were actually revealed or reconstructed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The broader testing principle is to vary the request format and interaction pattern rather than assuming that one refusal demonstrates that the system prompt is protected.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes These Tests Difficult
&lt;/h2&gt;

&lt;p&gt;The three tests share an important challenge: the model's visible response does not necessarily tell you where the information originated or which control failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model access versus application access
&lt;/h3&gt;

&lt;p&gt;A model may generate a plausible-looking secret without actually having access to it. Conversely, an application may retrieve confidential information and pass it into the model's context even when the model would not otherwise know that information.&lt;/p&gt;

&lt;p&gt;Testers should establish whether a disclosed value is real, whether the application had access to it, and how it reached the response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversation context and session boundaries
&lt;/h3&gt;

&lt;p&gt;A model may repeat information because it remains available in the current conversation context. That behavior is different from exposing information across isolated sessions or retaining it beyond the intended lifetime.&lt;/p&gt;

&lt;p&gt;Testing should account for conversation history, application memory, retrieval systems, and any explicit persistence mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple defensive layers
&lt;/h3&gt;

&lt;p&gt;An application may use input filters, output classifiers, retrieval permissions, redaction, and model-level safeguards.&lt;/p&gt;

&lt;p&gt;A successful or unsuccessful request may reflect the combined behavior of these components rather than the underlying model alone. Where practical, testers should inspect application logs, retrieval traces, and tool calls to identify which control accepted or rejected the request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration and repeatability
&lt;/h3&gt;

&lt;p&gt;Results can depend on the model, application instructions, available tools, conversation history, and sampling configuration.&lt;/p&gt;

&lt;p&gt;A single successful or unsuccessful attempt is not necessarily conclusive. Relevant tests should be repeated under representative configurations, with enough information recorded to make the results reproducible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These challenges make &lt;strong&gt;observability&lt;/strong&gt; important. Without knowing what information entered the model's context and what happened after generation, it can be difficult to distinguish an actual disclosure from a plausible but unsupported response.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Counts as a Finding
&lt;/h2&gt;

&lt;p&gt;Across these three tests, a vulnerability is confirmed when the application exposes information in a way that violates its intended security or privacy constraints. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The application reveals credentials, API keys, personal data, or confidential records to a user who should not have access to them.&lt;/li&gt;
&lt;li&gt;Information supplied in one context becomes accessible in another context where it should remain isolated.&lt;/li&gt;
&lt;li&gt;Sensitive input is passed to a downstream tool, service, or logging component that is not authorized to receive it.&lt;/li&gt;
&lt;li&gt;The application discloses protected system instructions, and that disclosure violates an established confidentiality requirement or creates a demonstrable security risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction between disclosure and impact matters. A model repeating a synthetic test string to the same user is not necessarily a vulnerability. Likewise, revealing a system prompt is not automatically a security incident if the prompt contains no protected information and its disclosure does not violate a meaningful requirement.&lt;/p&gt;

&lt;p&gt;For each finding, establish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; information was exposed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where&lt;/strong&gt; it originated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; could access it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which security boundary&lt;/strong&gt; failed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For input leakage specifically, an unsuccessful attempt to reproduce a sensitive value does not prove that the application has no retention or logging risks. Those require separate tests of the relevant data-handling mechanisms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Remediation, in Short
&lt;/h2&gt;

&lt;p&gt;These tests point toward several overlapping defensive principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protect sensitive data at the application layer
&lt;/h3&gt;

&lt;p&gt;Apply access controls before retrieving or exposing confidential information. Enforce authorization outside the model, minimize the data included in model context, and redact sensitive values from outputs where appropriate.&lt;/p&gt;

&lt;p&gt;Review training and retrieval pipelines for inadvertently exposed secrets, and ensure that the model receives only the information required for its task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolate sensitive user input
&lt;/h3&gt;

&lt;p&gt;Define how sensitive input should be handled throughout its lifecycle, including model context, conversation history, application logs, and downstream tool calls.&lt;/p&gt;

&lt;p&gt;Use synthetic test values to verify that redaction, normalization, and access controls behave as intended. Where sensitive information should not persist, enforce that requirement through application and data-retention controls rather than relying on the model to forget it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat system prompts as instructions, not secrets storage
&lt;/h3&gt;

&lt;p&gt;Avoid embedding credentials, API keys, or other sensitive values in system prompts. Limit unnecessary internal implementation details, and test whether confidential instructions can be reconstructed through direct requests, partial extraction, or contextual framing.&lt;/p&gt;

&lt;p&gt;Prompt design and model-level safeguards can reduce unwanted disclosure, but they should not be the only controls protecting confidential information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce least privilege and session isolation
&lt;/h3&gt;

&lt;p&gt;Restrict access to tools, documents, databases, and other resources according to the requesting user's permissions. Ensure that conversation state and retrieved information cannot cross user or session boundaries unintentionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor information flows
&lt;/h3&gt;

&lt;p&gt;Where practical, log and monitor access to sensitive data, model inputs and outputs, retrieval operations, and tool calls. Protect those logs appropriately and use them to investigate suspected disclosure without creating additional copies of sensitive information.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The central principle:&lt;/strong&gt; enforce confidentiality and authorization in the surrounding application. A model's willingness to refuse a request is useful, but it is not a replacement for those controls.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Tools for Testing This
&lt;/h2&gt;

&lt;p&gt;Several tools can help automate or structure information-disclosure testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/NVIDIA/garak" rel="noopener noreferrer"&gt;Garak&lt;/a&gt;:&lt;/strong&gt; includes probes for sensitive information disclosure and prompt-leakage-related behaviors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://promptfoo.dev" rel="noopener noreferrer"&gt;Promptfoo&lt;/a&gt;:&lt;/strong&gt; supports adversarial testing and evaluations for LLM applications, including custom tests for sensitive-data exposure and prompt disclosure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools are useful for generating test cases and repeating requests, but they should complement application-specific testing. A model that reproduces a test value or reveals an instruction is not necessarily vulnerable; the tester still needs to establish what information was exposed, whether the disclosure was unauthorized, and what security boundary should have prevented it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next in This Series
&lt;/h2&gt;

&lt;p&gt;This post covered three information-disclosure tests. Coming up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-05 / 10 / 11 / 12&lt;/strong&gt; — unsafe outputs, harmful-content bias, hallucinations, and toxic output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-06 / 13&lt;/strong&gt; — agentic behavior limits and over-reliance on AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-08 / 09&lt;/strong&gt; — embedding manipulation and model extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-14&lt;/strong&gt; — explainability and interpretability&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Official Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/OWASP/www-project-ai-testing-guide/blob/main/Document/content/tests/AITG-APP-03_Testing_for_Sensitive_Data_Leak.md" rel="noopener noreferrer"&gt;AITG-APP-03: Testing for Sensitive Data Leak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/OWASP/www-project-ai-testing-guide/blob/main/Document/content/tests/AITG-APP-04_Testing_for_Input_Leakage.md" rel="noopener noreferrer"&gt;AITG-APP-04: Testing for Input Leakage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/OWASP/www-project-ai-testing-guide/blob/main/Document/content/tests/AITG-APP-07_Testing_for_Prompt_Disclosure.md" rel="noopener noreferrer"&gt;AITG-APP-07: Testing for Prompt Disclosure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/" rel="noopener noreferrer"&gt;OWASP Top 10 for LLM Applications — LLM02: Sensitive Information Disclosure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://owasp.org/www-project-ai-testing-guide/" rel="noopener noreferrer"&gt;OWASP AI Testing Guide&lt;/a&gt; — the full AITG-APP category&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The previous post examined whether attacker-controlled input can change an AI application's behavior. This post examines whether the application exposes information that should remain protected.&lt;/p&gt;

&lt;p&gt;Sensitive data leakage, input leakage, and prompt disclosure can produce similar-looking outputs, but they involve different information sources and different security boundaries.&lt;/p&gt;

&lt;p&gt;The important testing question is therefore broader than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I make the model reveal something?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A useful test asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can the application expose information to someone or somewhere that should not receive it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Answering that question requires more than testing the model's response. It means tracing where information originates, how it enters the model's context, which tools and users can access it, and what happens to it after the model responds.&lt;/p&gt;

&lt;p&gt;Prompt design, output filtering, and model-level safeguards can all contribute to reducing disclosure. But effective protection ultimately depends on application-level access controls, appropriate data handling, session isolation, and a clear understanding of which information the model actually needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next in this series:&lt;/strong&gt; unsafe outputs, hallucinations, and toxic content — testing what happens when the problem isn't information disclosure, but the content the application generates.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>AI Security Testing 101: Prompt Injection and Indirect Prompt Injection</title>
      <dc:creator>Vanessa Sastre</dc:creator>
      <pubDate>Tue, 20 Oct 2026 22:45:35 +0000</pubDate>
      <link>https://dev.to/vanessa_sastre/ai-security-testing-101-prompt-injection-and-indirect-prompt-injection-50mb</link>
      <guid>https://dev.to/vanessa_sastre/ai-security-testing-101-prompt-injection-and-indirect-prompt-injection-50mb</guid>
      <description>&lt;p&gt;This is the first post in a new series on AI application security testing, following the structure of the OWASP AI Testing Guide's AI Application Testing category (AITG-APP). Each post in the series will take one test from that category and turn it into something practical: what the vulnerability actually looks like, how testers approach it, and what "vulnerable" means in concrete terms.&lt;/p&gt;

&lt;p&gt;We're starting where the guide starts: prompt injection, one of the most well-known AI application vulnerabilities, and a class of attack that can underpin many other AI-specific security risks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Direct Prompt Injection (AITG-APP-01)
&lt;/h2&gt;

&lt;p&gt;Prompt injection happens when attacker-controlled input changes an AI application's behavior in a way that conflicts with its intended instructions or security constraints. The input may attempt to make the model ignore higher-priority instructions, reveal information it should not disclose, or perform an action it was not intended to perform.&lt;/p&gt;

&lt;p&gt;A useful, though imperfect, analogy is SQL injection: in both cases, attacker-controlled input can influence behavior that the application intended to control. The analogy has limits, however. SQL injection exploits the interpretation of data as executable syntax, while prompt injection arises from the way language models process natural-language instructions and contextual information.&lt;/p&gt;

&lt;p&gt;The AITG-APP-01 methodology characterizes a prompt injection in terms of three elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instructions:&lt;/strong&gt; what the attacker actually wants the model to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A trigger:&lt;/strong&gt; the specific phrasing, framing, or encoding intended to cause the model to follow the attacker's instructions rather than its intended behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict with intent:&lt;/strong&gt; the injected request conflicts with the application's intended instructions or constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that conflict, an unusual or adversarial-looking prompt may simply be a normal request rather than a security-relevant prompt injection.&lt;/p&gt;

&lt;p&gt;Prompt injection is also broader than jailbreaking. A jailbreak attempts to bypass safety restrictions, while prompt injection more generally involves attacker-controlled input changing model behavior in an unintended way. An injection can therefore be security-relevant even when it does not produce prohibited content.&lt;/p&gt;

&lt;p&gt;Whether an attack succeeds can depend heavily on how the trigger is constructed. That's one reason simple keyword filtering is insufficient on its own. Blocking the phrase "ignore previous instructions" does little against the same underlying idea expressed through role-play, another language, obfuscation, or an encoded representation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack Patterns Worth Knowing
&lt;/h2&gt;

&lt;p&gt;The OWASP guide catalogs a broad set of concrete payload techniques. They can be grouped into a handful of practical patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  Persona and role-play framing
&lt;/h3&gt;

&lt;p&gt;Asking the model to "pretend" to be an unrestricted persona so that it treats otherwise disallowed behavior as being in character. A well-known example is the "DAN" jailbreak.&lt;/p&gt;

&lt;p&gt;The underlying technique is not limited to DAN. Attackers can construct fictional characters, simulated environments, alternate roles, or hypothetical scenarios intended to change how the model interprets its constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context manipulation
&lt;/h3&gt;

&lt;p&gt;Telling the model to "forget" previous instructions, attempting to override higher-priority context, or gradually steering the conversation toward a prohibited behavior.&lt;/p&gt;

&lt;p&gt;This can include multi-turn approaches in which no individual request appears particularly harmful, but the sequence gradually changes the context presented to the model. "Crescendo" and "echo chamber" style attacks are examples of this broader pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Obfuscation and encoding
&lt;/h3&gt;

&lt;p&gt;Hiding or transforming the attack instruction using techniques such as base64, hexadecimal encoding, reversed text, unusual capitalization, deliberate typos, Unicode manipulation, or other transformations.&lt;/p&gt;

&lt;p&gt;The purpose is often to evade filters or exploit differences between what a security control recognizes and what the model can interpret.&lt;/p&gt;

&lt;p&gt;The important lesson is that filtering for a fixed set of phrases is not a sufficient defense against a language-based attack surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural tricks
&lt;/h3&gt;

&lt;p&gt;Taking advantage of how the application or model handles structured inputs, including JSON or other formats, multi-message inputs, or instructions embedded within content that the application expects the model to process.&lt;/p&gt;

&lt;p&gt;Structured formats can create additional attack surfaces when applications treat model-generated or model-consumed fields as trusted simply because they conform to an expected schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contradiction and instruction-conflict framing
&lt;/h3&gt;

&lt;p&gt;Presenting multiple competing instructions or asking the model to produce both a normal response and an explicitly contrary response, with the goal of getting it to produce the latter.&lt;/p&gt;

&lt;p&gt;The exact payloads are less important than the general testing principle: don't assume that one known jailbreak or injection string represents the entire attack surface.&lt;/p&gt;

&lt;p&gt;These techniques are not permanent. Models, applications, and defenses evolve, so a payload that works against one model or configuration may become ineffective against another. That's why testing should combine techniques and repeat requests rather than relying on a single known payload.&lt;/p&gt;

&lt;p&gt;Response variability can also matter. In configurations where sampling introduces variability, the same prompt may produce different results across attempts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes a Test Environment Different
&lt;/h2&gt;

&lt;p&gt;Before running tests against a real target, it's worth understanding what's actually in front of you. The architecture and model configuration can significantly affect which techniques are relevant and how the results should be interpreted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temperature
&lt;/h3&gt;

&lt;p&gt;Lower temperature generally makes output more repeatable, while higher temperature generally increases variability and diversity.&lt;/p&gt;

&lt;p&gt;That matters during testing because a payload that succeeds once may not succeed consistently. Testers should therefore repeat relevant requests and record the configuration used rather than treating a single successful or unsuccessful attempt as definitive.&lt;/p&gt;

&lt;p&gt;Temperature does not by itself determine whether a system is secure, and a temperature of zero should not be treated as a universal guarantee of deterministic behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fine-tuning vs. prompt tuning
&lt;/h3&gt;

&lt;p&gt;Safety and behavioral constraints can be implemented at different layers, including through model training or fine-tuning and through application-level instructions or other steering mechanisms.&lt;/p&gt;

&lt;p&gt;This distinction matters during testing because a successful bypass may target different layers of the system. A test might circumvent application-level instructions without demonstrating that the underlying model has changed, for example.&lt;/p&gt;

&lt;p&gt;The important question for a tester is therefore not just "did the model comply?" but also "which security control was bypassed?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Guardrails
&lt;/h3&gt;

&lt;p&gt;Guardrails can be implemented in several ways, including deterministic rules, classifiers, access controls, schema validation, filtering systems, or additional LLM-based checks.&lt;/p&gt;

&lt;p&gt;When an application uses an LLM as part of its guardrail system, that introduces another model and potentially another source of variability. Testers should evaluate the guardrail as part of the attack surface rather than assuming that adding another model automatically prevents prompt injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-agent setups
&lt;/h3&gt;

&lt;p&gt;When several agents hand work to each other, testers may only see the final output rather than the intermediate prompts, tool calls, and responses.&lt;/p&gt;

&lt;p&gt;That makes it harder to determine which agent or handoff an injection actually influenced. Testing therefore benefits from observability into agent-to-agent messages, tool calls, retrieved content, and intermediate decisions wherever possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool access
&lt;/h3&gt;

&lt;p&gt;A model wired up to scripts, shells, databases, or external APIs can turn a successful injection from unwanted model output into an unauthorized action.&lt;/p&gt;

&lt;p&gt;Tool access can therefore substantially increase the potential impact of a prompt injection. However, tool access is not required for an injection to have security impact: unauthorized information disclosure or manipulation of important outputs can also be significant.&lt;/p&gt;




&lt;h1&gt;
  
  
  Indirect Prompt Injection (AITG-APP-02)
&lt;/h1&gt;

&lt;p&gt;Direct injection assumes the attacker is the one providing the malicious input to the AI application.&lt;/p&gt;

&lt;p&gt;Indirect prompt injection works differently. The malicious instruction is planted somewhere the AI application will process as external content: a web page, PDF, email, uploaded document, database record, search result, or tool-related content.&lt;/p&gt;

&lt;p&gt;The model may then encounter that instruction as part of the context it receives.&lt;/p&gt;

&lt;p&gt;The important distinction is not that the model has literally "no way" to distinguish data from instructions. Rather, the model may fail to reliably distinguish untrusted external content from instructions when both are represented in the context it processes.&lt;/p&gt;

&lt;p&gt;That creates a different attack surface: the attacker may not need direct access to the AI application if they can influence content that the application will eventually retrieve or process.&lt;/p&gt;

&lt;p&gt;Indirect injection can be particularly concerning in applications that automatically retrieve external content, have access to sensitive information, or can take actions through tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concrete Surfaces This Shows Up On
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hidden text in web pages or documents
&lt;/h3&gt;

&lt;p&gt;An attacker can place instructions in content that is difficult for a human reviewer to notice, such as HTML comments, invisible or visually obscured text, document metadata, or other content that is nevertheless extracted and passed to the model.&lt;/p&gt;

&lt;p&gt;The important security property is not that the text is literally invisible. It's that the application may process content that the user did not realize would be interpreted by the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured data fields
&lt;/h3&gt;

&lt;p&gt;A JSON, XML, database record, API response, or other structured input may contain an attacker-controlled field whose value includes an instruction rather than the data the application expected.&lt;/p&gt;

&lt;p&gt;For example, a field intended to contain a document summary or description might contain language such as an instruction to ignore the application's task and perform another action.&lt;/p&gt;

&lt;p&gt;The fact that the content is valid JSON or XML does not make it trustworthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown-based exfiltration
&lt;/h3&gt;

&lt;p&gt;An indirect prompt injection can attempt to make the model include sensitive information in a URL or other externally observable location, including by generating a Markdown image reference or link containing attacker-controlled data.&lt;/p&gt;

&lt;p&gt;If the resulting content is rendered or fetched by a downstream component, this can create a data-exfiltration path.&lt;/p&gt;

&lt;p&gt;A related technique was demonstrated in the real-world EchoLeak vulnerability affecting Microsoft 365 Copilot. EchoLeak was a multi-stage attack involving indirect prompt injection and data exfiltration under particular conditions; it should not be treated as synonymous with every Markdown-based exfiltration technique.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encoded payloads
&lt;/h3&gt;

&lt;p&gt;External content can also contain encoded or otherwise obfuscated instructions. The same general filter-evasion issues that apply to direct prompt injection can therefore apply to indirect injection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool metadata and tool poisoning
&lt;/h3&gt;

&lt;p&gt;For AI systems using MCP or similar tool-calling protocols, tool names, descriptions, schemas, metadata, parameters, and outputs can become part of the model's context.&lt;/p&gt;

&lt;p&gt;In some client implementations, this information may not be prominently exposed to the user even though it is presented to the model.&lt;/p&gt;

&lt;p&gt;An attacker who can control a tool's metadata may therefore attempt to embed instructions into it, for example, instructions telling the model to retrieve information before using the tool.&lt;/p&gt;

&lt;p&gt;The security issue is not specific to one metadata field. The broader concern is that the model may treat untrusted tool-related content as instructions.&lt;/p&gt;

&lt;p&gt;The common thread is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Anywhere external content reaches the model without a reliable boundary between trusted instructions and untrusted data, there is a potential prompt-injection attack surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That boundary is an architectural concern, not merely a question of finding the right payload string.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Counts as a Finding
&lt;/h1&gt;

&lt;p&gt;For both tests, a prompt injection becomes a security finding when attacker-controlled instructions cause behavior that violates the application's intended constraints or security boundaries.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model overrides or circumvents intended instructions in a security-relevant way.&lt;/li&gt;
&lt;li&gt;The application discloses information that the model or user should not have access to, such as credentials, API keys, internal configuration, or another user's data.&lt;/li&gt;
&lt;li&gt;The model or connected system performs an unauthorized or harmful action.&lt;/li&gt;
&lt;li&gt;An indirect injection causes the application to treat attacker-controlled external content as an instruction and subsequently produce unauthorized disclosure or action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For indirect injection specifically, the distinguishing condition is that the security-relevant behavior originates in externally supplied content rather than the user's direct prompt.&lt;/p&gt;

&lt;p&gt;Simply following an instruction found in external content is not automatically a vulnerability. For example, if a webpage contains ordinary instructions that are relevant to the user's request and the model follows them as part of legitimately processing that page, there may be no security issue.&lt;/p&gt;

&lt;p&gt;The important question is whether attacker-controlled external content can change the application's behavior in a way that violates its intended security or operational constraints.&lt;/p&gt;




&lt;h1&gt;
  
  
  Remediation, in Short
&lt;/h1&gt;

&lt;p&gt;Both tests point toward several overlapping defensive principles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate trusted instructions from untrusted content
&lt;/h3&gt;

&lt;p&gt;Clearly distinguish system/application instructions from user input and external data.&lt;/p&gt;

&lt;p&gt;Delimiters and structured context can help the model distinguish these sources, but they should be treated as mitigations rather than a guarantee that prompt injection will never succeed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat external content as untrusted
&lt;/h3&gt;

&lt;p&gt;Validate and sanitize external content before it reaches the model where appropriate. Consider hidden text, encoded instructions, unusual Unicode, metadata, and other forms of content that a human reviewer may not recognize as instructions.&lt;/p&gt;

&lt;p&gt;But don't rely on sanitization alone. Natural language is too flexible to make exhaustive filtering a reliable security boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restrict model privileges
&lt;/h3&gt;

&lt;p&gt;Give the model only the access and capabilities it actually needs.&lt;/p&gt;

&lt;p&gt;If a model does not need access to credentials, private files, sensitive databases, or administrative functions, those capabilities should not be available to it merely because the model might request them.&lt;/p&gt;

&lt;p&gt;Authorization should be enforced outside the model wherever possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Require approval for high-impact actions
&lt;/h3&gt;

&lt;p&gt;Sensitive, high-impact, or irreversible actions should require appropriate authorization or human approval rather than relying entirely on the model to decide whether an action is safe.&lt;/p&gt;

&lt;p&gt;The model should not be the sole security boundary protecting a privileged operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat tool metadata as untrusted input
&lt;/h3&gt;

&lt;p&gt;Tool descriptions, schemas, metadata, parameters, and outputs should be treated as potentially untrusted content.&lt;/p&gt;

&lt;p&gt;Applications should avoid assuming that a tool definition is safe simply because it was supplied through an otherwise trusted tool-calling protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add monitoring and observability
&lt;/h3&gt;

&lt;p&gt;Where practical, log and monitor retrieved content, model inputs, tool calls, authorization decisions, and significant outputs.&lt;/p&gt;

&lt;p&gt;This is particularly important in multi-agent systems, where a prompt injection may enter through one component and only become visible much later in the execution chain.&lt;/p&gt;




&lt;h1&gt;
  
  
  Tools for Testing This
&lt;/h1&gt;

&lt;p&gt;Several tools can help automate or structure prompt-injection testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Garak:&lt;/strong&gt; includes probes relevant to prompt injection and other LLM security testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promptfoo:&lt;/strong&gt; provides adversarial testing and evaluation capabilities for LLM applications, including prompt-injection testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt Security Fuzz (ps-fuzz):&lt;/strong&gt; a fuzzer focused on prompt-level attacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tools are useful for generating and repeating attacks, but they should complement application-specific testing. A generic payload that causes an unusual model response is not necessarily a vulnerability; the tester still needs to establish what security boundary was crossed and what the resulting impact is.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next in This Series
&lt;/h1&gt;

&lt;p&gt;This post covered the two prompt-manipulation tests. Coming up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-03 / 04 / 07&lt;/strong&gt; — sensitive data leaks, input leakage, and system prompt disclosure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-05 / 10 / 11 / 12&lt;/strong&gt; — unsafe outputs, harmful-content bias, hallucinations, and toxic output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-06 / 13&lt;/strong&gt; — agentic behavior limits and over-reliance on AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-08 / 09&lt;/strong&gt; — embedding manipulation and model extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AITG-APP-14&lt;/strong&gt; — explainability and interpretability&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Official Resources
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/OWASP/www-project-ai-testing-guide/blob/main/Document/content/tests/AITG-APP-01_Testing_for_Prompt_Injection.md" rel="noopener noreferrer"&gt;AITG-APP-01: Testing for Prompt Injection&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/OWASP/www-project-ai-testing-guide/blob/main/Document/content/tests/AITG-APP-01_Testing_for_Prompt_Injection.md" rel="noopener noreferrer"&gt;AITG-APP-02: Testing for Indirect Prompt Injection&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;OWASP Top 10 for LLM Applications — LLM01: Prompt Injection&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Prompt injection isn't a vulnerability that can necessarily be "patched once." It is a recurring security problem for systems that allow language models to process instructions alongside user-controlled or externally sourced content.&lt;/p&gt;

&lt;p&gt;Direct injection tests what happens when a user actively attempts to change the application's behavior. Indirect injection tests whether attacker-controlled external content can influence the system even when the attacker never interacts with the AI interface directly.&lt;/p&gt;

&lt;p&gt;The important security question is therefore broader than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I make the model ignore its system prompt?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A useful test asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can attacker-controlled input or external content cause the application to violate an intended security or operational constraint?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That framing also makes the impact easier to assess. A successful jailbreak that produces harmless text is different from an injection that exposes confidential information, bypasses authorization, or causes a connected system to perform an unauthorized action.&lt;/p&gt;

&lt;p&gt;Prompt injection techniques will continue to evolve as models, applications, and defenses change. Effective testing therefore needs to combine different attack patterns, repeat tests under representative configurations, and most importantly evaluate the actual security boundaries around the model rather than treating the model itself as the only control.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Playwright API Testing: Your API and UI Tests Can Live Together</title>
      <dc:creator>Vanessa Sastre</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:01:00 +0000</pubDate>
      <link>https://dev.to/vanessa_sastre/playwright-api-testing-why-your-api-and-ui-tests-should-live-together-3b6g</link>
      <guid>https://dev.to/vanessa_sastre/playwright-api-testing-why-your-api-and-ui-tests-should-live-together-3b6g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Playwright API testing&lt;/strong&gt; uses the same framework, the same Playwright project/configuration, and the same test suite you already write UI tests in to send real HTTP requests, mock responses, and verify the resulting server-side state after a UI action. No second tool, no second test runner, no context-switching between a UI suite and an API suite that drift out of sync with each other.&lt;/p&gt;

&lt;p&gt;That's the core shift this post covers: what changes when API testing stops being a separate discipline, how to set it up for pure API tests and for UI tests that need backend validation, and how this approach affects coverage, feedback, and CI cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Two Test Suites, One Application
&lt;/h2&gt;

&lt;p&gt;Most teams end up with API coverage and UI coverage living in different tools: Postman or a REST client for the API; Playwright or Selenium for the UI, because that's how the tooling has traditionally been split. That split has a cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicated setup.&lt;/strong&gt; Auth, base URLs, and test data creation get written twice, once per suite, and drift apart over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind spots at the seam.&lt;/strong&gt; UI tests confirm a button click &lt;em&gt;looked&lt;/em&gt; successful. API tests confirm an endpoint works &lt;em&gt;in isolation&lt;/em&gt;. Neither one confirms that clicking the button actually triggered the right backend call with the right payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow, UI-only precondition setup.&lt;/strong&gt; To test "issue #42 shows correctly," a UI-only suite has to create that issue by clicking through the UI first, and one more brittle flow that can fail before the actual test even starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shared debugging story.&lt;/strong&gt; When something breaks at the UI/API boundary, you're reconciling two separate tools' worth of logs to find out where the mismatch happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a tooling limitation you have to accept, it's a consequence of treating "API testing" and "UI testing" as different jobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: One Framework, One Request Context
&lt;/h2&gt;

&lt;p&gt;Playwright provides the same API-testing primitives for both pure API tests and UI tests that need backend setup or validation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Split-tooling approach&lt;/th&gt;
&lt;th&gt;With Playwright API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test an endpoint directly&lt;/td&gt;
&lt;td&gt;Separate Postman collection or REST client, maintained apart from the UI suite&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;request&lt;/code&gt; fixture test, in the same repo, same config, same CI job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed data before a UI test&lt;/td&gt;
&lt;td&gt;Click through the UI to create it, or maintain a separate seeding script&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;request.post()&lt;/code&gt; call in &lt;code&gt;beforeAll&lt;/code&gt;, reusing the same auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate a UI action's server-side result&lt;/td&gt;
&lt;td&gt;Trust the UI state, or manually check the database/API afterward&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;request.get()&lt;/code&gt; after the UI action, asserting on the real server-side state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep cookie/local-storage auth in sync between suites&lt;/td&gt;
&lt;td&gt;Separate auth setup&lt;/td&gt;
&lt;td&gt;Reuse Playwright &lt;code&gt;storageState&lt;/code&gt; between API and browser contexts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Pure API Testing
&lt;/h2&gt;

&lt;p&gt;For endpoint-level tests with no browser involved, configure a base URL and headers once, and every test gets a ready-to-use &lt;code&gt;request&lt;/code&gt; fixture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;extraHTTPHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.github.v3+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`token &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should create a bug report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newIssue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/repos/USER/REPO/issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Bug] report 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bug description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newIssue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/repos/USER/REPO/issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toContainEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Bug] report 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup and teardown for test data can use the built-in request fixture in beforeAll/afterAll. If you create a standalone APIRequestContext yourself with playwright.request.newContext(), dispose of it in afterAll.&lt;/p&gt;




&lt;h2&gt;
  
  
  Combined UI + API: Validating That Actions Trigger Backend Calls
&lt;/h2&gt;

&lt;p&gt;This is where the split-tooling approach falls down hardest and where a single framework pays off the most. Two patterns cover most cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Establishing preconditions via API, before touching the UI
&lt;/h3&gt;

&lt;p&gt;Create state through the API, then confirm it's reflected correctly in the UI without clicking through slow setup flows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;apiContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;playwright&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;apiContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;playwright&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;extraHTTPHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`token &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last created issue should be first in the list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newIssue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apiContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/repos/USER/REPO/issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Feature] request 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newIssue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/USER/REPO/issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`a[data-hovercard-type='issue']`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Feature] request 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test only ever clicks through the one flow it's actually testing, the setup happens instantly over the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validating postconditions after a UI action
&lt;/h3&gt;

&lt;p&gt;Drive the interaction through the UI, then confirm the backend actually recorded it, not just that the UI rendered a success state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;creating an issue via the UI should persist on the server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/USER/REPO/issues&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Issue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bug report 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submit new issue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issueId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newIssue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apiContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/repos/USER/REPO/issues/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newIssue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;newIssue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;objectContaining&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bug report 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the check a UI-only test structurally can't do: it proves the click didn't just update local state or a client-side cache, it verifies that the UI action resulted in the expected server-side state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing auth between both
&lt;/h3&gt;

&lt;p&gt;Log in once via the API, save the resulting &lt;code&gt;storageState&lt;/code&gt;, and reuse it in a browser context, one login flow instead of two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;storageState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;state.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mocking: Testing States You Can't Easily Produce
&lt;/h2&gt;

&lt;p&gt;Not every test should hit a real backend. Playwright can intercept, modify, or fully mock network traffic at the browser level with &lt;code&gt;page.route()&lt;/code&gt;, useful for error states, edge cases, and tests that shouldn't depend on a live API being up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mock a response entirely:&lt;/strong&gt; the real API is never called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*/**/api/v1/fruits&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Strawberry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Call the real API, then patch the response:&lt;/strong&gt; useful when you need mostly-real data with one controlled edge case injected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*/**/api/v1/fruits&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loquat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fulfill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replay from a recorded HAR file:&lt;/strong&gt; record real traffic once, then run tests against it offline and deterministically, with no live dependency at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routeFromHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./hars/fruit.har&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*/**/api/v1/fruits&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These interactions are visible in Playwright's trace/network tooling, making mocked and live network behavior debuggable from the same workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beyond HTTP:&lt;/strong&gt; Playwright can also intercept and mock WebSocket connections with page.routeWebSocket() for real-time features. For browser APIs that Playwright doesn't directly automate, page.addInitScript() can be used to install mocks before the page's own code runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Efficiency Case
&lt;/h2&gt;

&lt;p&gt;For a team deciding whether to consolidate API and UI testing into one framework, these are the levers that actually move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One suite to maintain, not two.&lt;/strong&gt; Auth, base URLs, and test data setup exist in a single place instead of being reimplemented per tool, every hour spent keeping a Postman collection and a Playwright suite in sync is an hour not spent on coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster test setup, lower CI time.&lt;/strong&gt; API-based setup is typically much faster than reproducing the same state through a browser workflow, because it avoids navigation, rendering, and UI interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fewer false negatives.&lt;/strong&gt; A UI test that only checks for a success toast can pass while the backend call silently failed. Postcondition checks against the real API catch that class of bug before a customer does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic tests, fewer flaky-test investigations.&lt;/strong&gt; Mocked and HAR-replayed tests aren't affected by a slow or temporarily-down backend, which removes one of the most common sources of "flaky" CI failures that cost engineering time to triage and usually turn out to be infrastructure, not the code under test.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  CI/CD Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep pure API tests in their own project/job.&lt;/strong&gt; They're fast and don't need a browser, running them separately from UI tests shortens feedback on backend-only regressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use HAR replay for UI tests that don't need live data.&lt;/strong&gt; It removes a live dependency from the critical path of your CI pipeline, and makes those tests immune to unrelated backend outages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve postcondition checks for flows where "the UI looked right" genuinely isn't enough proof.&lt;/strong&gt; Checkout, payments, anything where a silent backend failure would be expensive. Applying it everywhere adds maintenance cost without a matching increase in the bugs it catches.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Official Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/api-testing" rel="noopener noreferrer"&gt;API testing&lt;/a&gt; — &lt;code&gt;APIRequestContext&lt;/code&gt;, setup/teardown, sharing auth between API and browser contexts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/mock" rel="noopener noreferrer"&gt;Mock APIs&lt;/a&gt; — mocking requests, modifying responses, HAR file recording and replay&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/mock-browser-apis" rel="noopener noreferrer"&gt;Mock browser APIs&lt;/a&gt; — mocking browser-level APIs Playwright doesn't automate directly&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/network" rel="noopener noreferrer"&gt;Network&lt;/a&gt; — request/response interception, modification, and WebSocket mocking in depth&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Playwright API testing isn't about replacing every API test with a browser test, or using one tool simply for the sake of consolidation. It's about having the flexibility to test the right layer with the same framework.&lt;/p&gt;

&lt;p&gt;Pure API tests can stay fast and focused. UI tests can use the API to create preconditions or verify server-side outcomes. Network mocking and HAR replay can remove unnecessary dependencies from UI tests. And because all of these live within the same Playwright project, they can share configuration, authentication, test data strategies, debugging tools, and CI infrastructure.&lt;/p&gt;

&lt;p&gt;The result is a testing strategy that's easier to compose: use the API where it makes sense, use the browser where it matters, and combine them when testing one without the other leaves gaps.&lt;/p&gt;




</description>
      <category>playwright</category>
      <category>testing</category>
      <category>api</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Playwright Trace Viewer: Debug Failed Tests Without Reproducing Them</title>
      <dc:creator>Vanessa Sastre</dc:creator>
      <pubDate>Thu, 06 Aug 2026 21:57:33 +0000</pubDate>
      <link>https://dev.to/vanessa_sastre/playwright-trace-viewer-debug-failed-tests-without-reproducing-them-39o0</link>
      <guid>https://dev.to/vanessa_sastre/playwright-trace-viewer-debug-failed-tests-without-reproducing-them-39o0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Playwright Trace Viewer&lt;/strong&gt; is a built-in GUI that records a full, replayable snapshot of a test run — every action, DOM state, network call, and console log — so you can debug a CI failure by opening a file, not by reproducing it.&lt;/p&gt;

&lt;p&gt;In practice, that makes a UI test into more than a UI recording. Every browser interaction is synchronized with the underlying API traffic, console output, and page state at the moment it happened — which gives you an API debugging layer for free, without writing a single API test.&lt;/p&gt;

&lt;p&gt;That one difference changes the economics of test debugging. This post covers what changes in your day-to-day workflow, how to turn it on, and how to justify it to the people who care about engineering time and CI spend rather than DOM snapshots.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Debugging Requires Reproducing
&lt;/h2&gt;

&lt;p&gt;CI fails → Try locally → Can't reproduce → Add debugging → Implement a fix → Push → Wait for CI → Repeat&lt;/p&gt;

&lt;p&gt;A test fails on CI. Here's what that traditionally costs you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the error message and stack trace, which often aren't clear enough on their own.&lt;/li&gt;
&lt;li&gt;Pull the branch and try to reproduce the failure locally.&lt;/li&gt;
&lt;li&gt;It passes locally. CI-only failures are common — different browser, viewport, timing, environment, or data state.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;console.log&lt;/code&gt;, screenshots, tracing, or &lt;code&gt;page.pause()&lt;/code&gt; calls and run the test again.&lt;/li&gt;
&lt;li&gt;It still doesn't reproduce. Try running headed, slow down actions, add waits, or tweak the environment until it finally fails.&lt;/li&gt;
&lt;li&gt;Eventually reproduce the issue, identify the root cause, remove the debugging code, and implement a fix.&lt;/li&gt;
&lt;li&gt;Push the fix and wait for CI to run again. If the failure persists, repeat the process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Depending on the bug, that loop can take anywhere from a few minutes to most of an afternoon — and it scales linearly with how flaky or environment-dependent the failure is. Every step exists because you're missing information that already existed at the moment the test ran, and is now gone.&lt;/p&gt;

&lt;p&gt;Trace Viewer's premise: &lt;strong&gt;stop throwing that information away.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Open the Trace Instead of Re-Running the Test
&lt;/h2&gt;

&lt;p&gt;With tracing enabled, every action, DOM snapshot, request, and log from the run is captured into a single &lt;code&gt;trace.zip&lt;/code&gt;. When a test fails, you open that file — locally, from CI, or in the browser — and the run replays exactly as it happened.&lt;/p&gt;

&lt;p&gt;Here's the same debugging tasks, before and after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Without Trace Viewer&lt;/th&gt;
&lt;th&gt;With Trace Viewer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Find why a click hit the wrong element&lt;/td&gt;
&lt;td&gt;Re-run headed, add logs, hope it reproduces&lt;/td&gt;
&lt;td&gt;Click the action, see the before/after DOM and the exact click coordinates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grab a locator for a new assertion&lt;/td&gt;
&lt;td&gt;Re-run in debug mode, use the inspector live&lt;/td&gt;
&lt;td&gt;Open the &lt;strong&gt;Locator&lt;/strong&gt; tab on the already-captured screen and pick it — no re-run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnose a failed network call&lt;/td&gt;
&lt;td&gt;Add request logging, re-run, inspect devtools&lt;/td&gt;
&lt;td&gt;Open &lt;strong&gt;Network&lt;/strong&gt;, already filtered to that action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find the exact line of code behind a step&lt;/td&gt;
&lt;td&gt;Search the file, count steps manually&lt;/td&gt;
&lt;td&gt;Click the action, &lt;strong&gt;Source&lt;/strong&gt; jumps to that line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Investigate a CI-only flake&lt;/td&gt;
&lt;td&gt;SSH into CI, add debug output, wait for the next occurrence&lt;/td&gt;
&lt;td&gt;Download the trace CI already attached, open it locally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern across every row: instead of recreating the conditions of the failure, you're looking at a recording of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup: Two Lines of Config
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Locally&lt;/strong&gt;, trace everything while you're developing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use UI Mode, which traces every test automatically without the flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On CI&lt;/strong&gt;, tracing every run is wasteful — trace only what fails, on retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on-first-retry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other values: &lt;code&gt;on-all-retries&lt;/code&gt;, &lt;code&gt;retain-on-failure&lt;/code&gt; (if you don't use retries), &lt;code&gt;on&lt;/code&gt; (every run — expensive), &lt;code&gt;off&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opening a trace:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From the HTML report: click &lt;strong&gt;View trace&lt;/strong&gt; on any test — it opens a local instance of Trace Viewer directly, no file hunting.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  npx playwright show-report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;From the CLI, pointing at a file or a URL:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  npx playwright show-trace path/to/trace.zip
  npx playwright show-trace https://example.com/trace.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;In the browser, no install required: &lt;a href="https://trace.playwright.dev" rel="noopener noreferrer"&gt;trace.playwright.dev&lt;/a&gt;. Drag in a file, or pass one as a URL query parameter — handy for linking straight from a CI artifact. Everything renders locally in your browser; nothing is uploaded.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Inside a Trace
&lt;/h2&gt;

&lt;p&gt;A timeline runs across the top of the viewer — click or drag anywhere on it, and every panel below updates to that moment. You never have to click through actions one by one to find the point you care about.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tab&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every action, its locator, and its duration, with before/after DOM snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Locator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The exact screen, fully replicated, with Pick Locator available on it — build a new locator without executing anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The exact line of test code behind the action you're viewing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Call&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Timing, resolved locator, and mode (e.g. strict) for the selected action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Log&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playwright's internal narration — waiting for visible/enabled/stable, then acting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Errors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The failure message, with a red marker on the timeline showing exactly when it happened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Console&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser and test console logs, filterable to a single action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every request with headers/bodies, filterable to a single action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metadata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser, viewport, duration, and run context in one place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Attachments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Expected vs. actual image diffs with a slider, for visual regression tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Network&lt;/strong&gt; tab is easy to underestimate. This is the API debugging layer mentioned at the top of this post: even if your suite only exercises the UI, every backend call made during the test is captured and synchronized with the user's actions. Instead of opening DevTools and trying to reproduce the issue, you already have a complete timeline of both the frontend behavior and the API interactions that produced it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Traces as Context for AI Agents
&lt;/h2&gt;

&lt;p&gt;Everything a trace captures — actions, locators, DOM snapshots, network calls, console output — is structured data sitting in a zip file, not just a GUI to click through manually. That makes it a good fit for AI coding agents and testing skills that need to diagnose a failure without a human first reproducing it.&lt;/p&gt;

&lt;p&gt;A few ways this shows up in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous failure triage.&lt;/strong&gt; An agent can be handed a &lt;code&gt;trace.zip&lt;/code&gt; from a CI run and asked to identify the failing action, the locator involved, and the DOM state around it, then propose a fix — without ever spinning up a browser or re-running the suite itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root-cause summaries instead of raw logs.&lt;/strong&gt; Rather than pasting a stack trace into a chat, an agent (or a skill built around one) can walk the Actions, Network, and Console data from a trace and produce a plain-language explanation of what broke and where, which is a much shorter path to a fix than reading raw output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locator generation without execution.&lt;/strong&gt; Because the Locator tab replicates the exact screen at a given moment, an agent can use that captured state to derive or validate a selector for a new assertion, the same way a human uses Pick Locator — without needing a live browser session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feeding "video receipts" back in.&lt;/strong&gt; If you're also recording screencasts (Playwright's &lt;code&gt;page.screencast&lt;/code&gt; API) alongside traces, an agent's own verification steps can be captured the same way — giving you a reviewable artifact of what an agent did, not just a claim that it passed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI feedback loops.&lt;/strong&gt; Since traces are already CI artifacts with stable URLs, an agent watching a pipeline can fetch a failing test's trace automatically as soon as it's produced, rather than waiting on a person to attach logs or describe the failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying shift is the same one this post keeps coming back to: a trace turns "the test failed" into a self-contained record that doesn't require a human — or an agent — to reproduce the failure to understand it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Efficiency Case
&lt;/h2&gt;

&lt;p&gt;Trace Viewer isn't just a developer-experience nicety — it changes measurable costs. If you need to justify adopting it (or prioritizing better trace coverage in CI), these are the levers to point at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mean time to resolution (MTTR) on test failures.&lt;/strong&gt; The reproduction loop above is where most debugging time goes. Removing the "make it fail again" step is the single biggest lever on how long a failure sits open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineer context-switching cost.&lt;/strong&gt; A flaky CI failure that can be diagnosed by opening a file, versus one that requires blocking time to reproduce locally, has a very different interruption cost — especially for failures that surface hours after the engineer has moved to other work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI compute spend.&lt;/strong&gt; &lt;code&gt;trace: 'on-first-retry'&lt;/code&gt; only records what actually fails, so you get debugging data without paying to trace every green run. Compare that to teams that resort to re-running whole suites repeatedly to catch a flake in the act.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flaky test triage throughput.&lt;/strong&gt; Because traces are attachable CI artifacts, failures can be triaged asynchronously by whoever has time, not only by whoever can reproduce the environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to track the impact concretely, two numbers are enough to start: &lt;strong&gt;time from failure to root cause identified&lt;/strong&gt;, and &lt;strong&gt;number of "couldn't reproduce, closed as flaky"&lt;/strong&gt; tickets. Both should move once trace review replaces manual reproduction as the default first step.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace on retry, not on every run.&lt;/strong&gt; &lt;code&gt;on-first-retry&lt;/code&gt; (or &lt;code&gt;retain-on-failure&lt;/code&gt; without retries) gives full debugging data for failures at a fraction of the storage and runtime cost of &lt;code&gt;on&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link traces directly from CI output.&lt;/strong&gt; Since &lt;code&gt;show-trace&lt;/code&gt; and &lt;code&gt;trace.playwright.dev&lt;/code&gt; both accept URLs, wire your CI's artifact link straight into a one-click trace view instead of a manual download-and-unzip step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat "couldn't reproduce" as a process gap, not bad luck.&lt;/strong&gt; If a failure recurs without a trace attached, that's a signal to widen trace coverage, not just re-run the suite again.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Official Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/trace-viewer" rel="noopener noreferrer"&gt;Trace viewer documentation&lt;/a&gt; — full walkthrough of every tab and feature&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/api/class-testoptions#test-options-trace" rel="noopener noreferrer"&gt;testOptions.trace&lt;/a&gt; — configuration reference&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://trace.playwright.dev" rel="noopener noreferrer"&gt;trace.playwright.dev&lt;/a&gt; — the hosted viewer, for opening traces without the CLI&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;Playwright documentation&lt;/a&gt; — general guides, including CI configuration&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Most test-debugging time isn't spent fixing bugs — it's spent recreating the conditions that exposed them.&lt;/strong&gt; Trace Viewer removes that step by keeping a full record of the run itself, so the question shifts from &lt;em&gt;"can I reproduce this?"&lt;/em&gt; to &lt;em&gt;"what does the trace show?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For engineering teams, the value isn't measured by prettier debugging tools — it's measured by fewer engineering hours lost to reproducing failures. Shorter debugging cycles, fewer unnecessary CI reruns, and faster root-cause analysis are improvements that show up in both developer productivity and infrastructure costs.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>ai</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Stop Debugging Blind: Playwright Screencast API for Richer Test Evidence</title>
      <dc:creator>Vanessa Sastre</dc:creator>
      <pubDate>Wed, 22 Jul 2026 22:13:02 +0000</pubDate>
      <link>https://dev.to/vanessa_sastre/stop-debugging-blind-playwright-screencast-api-for-richer-test-evidence-47l</link>
      <guid>https://dev.to/vanessa_sastre/stop-debugging-blind-playwright-screencast-api-for-richer-test-evidence-47l</guid>
      <description>&lt;p&gt;When an automated test fails, the error message alone is often insufficient to diagnose the root cause.&lt;/p&gt;

&lt;p&gt;You're usually left asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What actions happened before the failure?&lt;/li&gt;
&lt;li&gt;Which element was being interacted with?&lt;/li&gt;
&lt;li&gt;Where exactly did the app start behaving differently than expected?&lt;/li&gt;
&lt;li&gt;Can a teammate understand the failure without re-running the test themselves?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Playwright 1.59 introduced &lt;code&gt;page.screencast&lt;/code&gt;, a programmatic recording API that answers these questions by turning a test run into a narrated, annotated video — instead of just a pass/fail line in a report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With screencast-based reporting, a test can produce:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A recording of the full execution&lt;/li&gt;
&lt;li&gt;Visual annotations on every interacted element&lt;/li&gt;
&lt;li&gt;Chapter markers that break the run into named stages&lt;/li&gt;
&lt;li&gt;Custom HTML overlays for context or metadata&lt;/li&gt;
&lt;li&gt;Real-time JPEG frame capture for external tooling&lt;/li&gt;
&lt;li&gt;"Video receipts" for AI-driven test and coding agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple: &lt;strong&gt;make failures faster to understand, and make passing runs easier to trust.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgmmpfawrn2nflpwo3hm5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgmmpfawrn2nflpwo3hm5.gif" alt="Screencast example showing annotated Playwright test execution" width="500" height="312"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Screencasts Matter
&lt;/h2&gt;

&lt;p&gt;Traditional artifacts already carry useful information:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What the framework reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Screenshots&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The app's state at one moment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOM snapshots, network activity, console output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A screencast adds a fourth layer that none of these provide well on their own: &lt;strong&gt;the full user journey, as a video.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of staring at this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected: "Order completed"
Received: "Payment failed"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...you watch the actual sequence: checkout opens, a product is added, payment details are entered, submit is clicked, and the error appears, all annotated in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screencast vs. Playwright Trace
&lt;/h2&gt;

&lt;p&gt;These two tools solve different problems, and a mature framework should use both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace Viewer&lt;/strong&gt; is built for deep technical debugging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network activity&lt;/li&gt;
&lt;li&gt;DOM snapshots&lt;/li&gt;
&lt;li&gt;Console messages&lt;/li&gt;
&lt;li&gt;Step-by-step execution inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Screencast&lt;/strong&gt; is built for fast, visual understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A timeline of what a user (or agent) actually did&lt;/li&gt;
&lt;li&gt;Visible, annotated interactions&lt;/li&gt;
&lt;li&gt;Context that non-technical stakeholders can follow without opening dev tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice: reach for &lt;strong&gt;traces&lt;/strong&gt; when you need to diagnose &lt;em&gt;why&lt;/em&gt; something broke, and for &lt;strong&gt;screencasts&lt;/strong&gt; when you need to show &lt;em&gt;what happened&lt;/em&gt; quickly, and to a wider audience than just engineers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Screencast Recording
&lt;/h2&gt;

&lt;p&gt;The cleanest way to roll this out across a suite is a shared fixture, so every test gets recording, annotations, and reporting for free — no boilerplate in individual test files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fixtures.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;videoPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screencast.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;videoPath&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showActions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top-right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Screencast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;videoPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every test that imports &lt;code&gt;test&lt;/code&gt; from this file now automatically records a screencast and attaches it to the report, including in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Narration Inside a Test
&lt;/h3&gt;

&lt;p&gt;Once the fixture is in place, use &lt;code&gt;showChapter()&lt;/code&gt; to label the stages of a test as it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User authentication flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...login steps&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Complete purchase flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// ...checkout steps&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting video reads like a labeled walkthrough rather than raw, unexplained footage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screencast API Reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;page.screencast.start(options)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Starts a screencast session. Pass &lt;code&gt;path&lt;/code&gt; to save a WebM recording, or &lt;code&gt;onFrame&lt;/code&gt; to receive JPEG frames in real time — both can be used together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;execution.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's called explicitly, you control exactly when recording starts and stops, unlike the older, always-on &lt;code&gt;video&lt;/code&gt; config option, which records for the entire test lifecycle regardless of what you actually need.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;page.screencast.stop()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stops the active session and finalizes the video file at the path given to &lt;code&gt;start()&lt;/code&gt;. Typically called right after the test body finishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;page.screencast.showActions(options)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Annotates interacted elements as they're clicked, filled, or hovered, so a reviewer can see exactly which element was touched and when.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showActions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top-right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ms each annotation stays visible&lt;/span&gt;
  &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pointer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// animates a cursor between actions; use 'none' to disable&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid &lt;code&gt;position&lt;/code&gt; values: &lt;code&gt;top-left&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;top-right&lt;/code&gt;, &lt;code&gt;bottom-left&lt;/code&gt;, &lt;code&gt;bottom&lt;/code&gt;, &lt;code&gt;bottom-right&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To turn annotations off mid-test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hideActions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Suite-wide alternative:&lt;/strong&gt; if you want action annotations everywhere without touching a fixture, enable them directly in &lt;code&gt;playwright.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top-left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top-right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;page.screencast.showChapter(title, options)&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Displays a centered, blurred-backdrop overlay to mark a new stage of the recording. Disappears automatically after &lt;code&gt;duration&lt;/code&gt; (defaults to a couple of seconds).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment validation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submitting card details and confirming charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaining a few of these gives you a scannable timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Login → Search product → Add to cart → Checkout → Payment validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reviewers can jump straight to the stage they care about instead of scrubbing a long recording blindly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;page.screencast.showOverlay(html, options)&lt;/code&gt; / &lt;code&gt;showOverlays()&lt;/code&gt; / &lt;code&gt;hideOverlays()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Adds arbitrary HTML on top of the recording — handy for environment info, build numbers, or debug messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showOverlay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;div style="color:red"&amp;gt;Running checkout validation&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omit &lt;code&gt;duration&lt;/code&gt; and the overlay stays until you explicitly hide or remove it. &lt;code&gt;showOverlays()&lt;/code&gt; / &lt;code&gt;hideOverlays()&lt;/code&gt; toggle visibility of overlays already on screen without discarding them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Frame Capture
&lt;/h3&gt;

&lt;p&gt;Pass &lt;code&gt;onFrame&lt;/code&gt; to &lt;code&gt;start()&lt;/code&gt; to stream JPEG-encoded frames while the test runs, instead of (or alongside) writing a video file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;onFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewportWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewportHeight&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;processFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewportWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewportHeight&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens the door to live dashboards, vision-model analysis, or custom monitoring pipelines that react to what's on screen &lt;em&gt;during&lt;/em&gt; execution, not after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agentic Video Receipts
&lt;/h2&gt;

&lt;p&gt;Screencasts are also a good fit for AI-driven testing and coding agents, where a plain text summary ("verified checkout — passed") is much less convincing than a video showing exactly what the agent did.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;receipt.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showActions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top-right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verifying checkout flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Validating payment completion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Agent performs its verification steps here&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#coupon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SAVE20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#apply-coupon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.discount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;20%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showChapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Checkout validation completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screencast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is a self-contained "receipt": chapter titles explain intent, action annotations show exactly what was clicked or filled, and a human can review the whole thing in the time it takes to watch a short clip, no re-running the agent required.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD Recommendations
&lt;/h2&gt;

&lt;p&gt;Recording every single test run adds up fast in storage and CI time. A few practical guardrails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record failures, not everything.&lt;/strong&gt; Attach the screencast only when a test doesn't match its expected status:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expectedStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Screencast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;videoPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record critical paths deliberately.&lt;/strong&gt; Reserve always-on recording for your highest-value flows (checkout, sign-up, payment) rather than every spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up old artifacts&lt;/strong&gt; on a schedule so recordings don't silently pile up in CI storage.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Official Resources
&lt;/h2&gt;

&lt;p&gt;This post covers the common patterns, but the official docs are the source of truth for full option lists, edge cases, and future updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/api/class-screencast" rel="noopener noreferrer"&gt;Screencast API reference&lt;/a&gt; — full method and options list&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/microsoft/playwright/releases" rel="noopener noreferrer"&gt;Playwright release notes&lt;/a&gt; — see the v1.59 entry for the original &lt;code&gt;page.screencast&lt;/code&gt; announcement, and later releases for additions like the &lt;code&gt;cursor&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;Playwright documentation&lt;/a&gt; — general guides, including video/trace configuration
If you hit something this post doesn't cover, those are the pages to check first.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Test automation is moving past a simple pass/fail line. Screencast-based reporting gives teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster failure diagnosis&lt;/li&gt;
&lt;li&gt;A visual record of execution flow anyone can follow, technical or not&lt;/li&gt;
&lt;li&gt;Better handoffs between QA and development&lt;/li&gt;
&lt;li&gt;A foundation for reviewing AI-driven testing and coding agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mature framework shouldn't just report &lt;em&gt;that&lt;/em&gt; a test failed, it should show &lt;strong&gt;what happened, where it happened, and give you what you need to fix it.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>tutorial</category>
      <category>documentation</category>
    </item>
  </channel>
</rss>
