Building Intelligent UI Automation: How Actors and Interactions Shape Workflow PatternsMastering the Actor-Interaction model to create flexible, maintainable test automation with deterministic and AI-driven workflows

Introduction: The Evolution Beyond Page Objects

We've all been there. Your test suite breaks because a developer changed a button's CSS class. You spend hours updating selectors across dozens of test files. The Page Object Model promised organization, but it delivered brittle hierarchies that collapse under the weight of modern application complexity. The fundamental problem isn't the pattern itself—it's that we've been thinking about automation at the wrong level of abstraction.

The Screenplay pattern, introduced by Antony Marcano, Jan Molak, and the SerenityJS team around 2016, shifts the perspective entirely. Instead of organizing code around page structures, it models automation around what users actually do — their tasks, their goals, their interactions. At its core lies the Actor-Interaction model: actors represent users with specific abilities, and interactions represent the actions and questions those actors can perform. This isn't just semantic reorganization; it's a fundamental rethinking that enables something remarkable. By decoupling what we want to accomplish from how we accomplish it, we create space for both deterministic workflows (where every step is predictable) and non-deterministic AI-driven workflows (where the system adapts based on context). This duality is what makes the pattern particularly powerful for modern automation challenges.

Understanding the Actor-Interaction Foundation

The Actor-Interaction model draws inspiration from both theatrical performance and behavioral design patterns. An Actor is any entity that performs actions within your system—typically representing a user persona like "admin user," "customer," or "guest visitor." Each actor possesses Abilities, which are the low-level capabilities they can exercise: the ability to browse the web using a browser, the ability to call APIs, the ability to access a database. These abilities are what make an actor functional, but they're intentionally generic and reusable.

Interactions sit one level above abilities and represent the atomic actions users can take. These include Tasks (multi-step activities like "login to the application" or "add item to cart"), Actions (single-step UI interactions like "click button" or "fill field"), and Questions (queries about application state like "is the error message visible?" or "what is the current cart total?"). The beauty of this layering is composability. Tasks are built from Actions and Questions. Workflows are built from Tasks. Each layer remains isolated, testable, and maintainable. When a UI change occurs, you modify the Action that targets a specific element, and every Task using that Action inherits the fix automatically. This is the promise Page Objects made but couldn't keep—because Page Objects bundle structure with behavior, while the Screenplay pattern separates them.

Here's what a basic actor setup looks like in TypeScript using the SerenityJS framework:

import { Actor, Cast, TakeNotes } from '@serenity-js/core';
import { BrowseTheWeb } from '@serenity-js/web';
import { CallAnApi } from '@serenity-js/rest';

class TestCast implements Cast {
  prepare(actor: Actor): Actor {
    return actor.whoCan(
      BrowseTheWeb.using(browser),
      CallAnApi.at('https://api.example.com'),
      TakeNotes.usingAnEmptyNotepad(),
    );
  }
}

// Creating an actor with abilities
const james = Actor.named('James').whoCan(
  BrowseTheWeb.using(browser),
  TakeNotes.usingAnEmptyNotepad()
);

This separation of concerns creates what software architects call "high cohesion and loose coupling." Each interaction focuses on one thing and does it well. Each actor configuration can be shared across tests without duplication. The pattern respects the Open/Closed Principle—you extend behavior by adding new Tasks or Actions, not by modifying existing ones.

Deterministic Workflows: Predictability Through Choreography

Deterministic workflows are the bread and butter of test automation—sequences where every step follows a predetermined path, where inputs produce expected outputs, where the execution is reproducible. In the Actor-Interaction model, deterministic workflows are compositions of Tasks and Questions that execute linearly. Think of these as choreographed routines: the actor performs step A, then step B, then validates condition C. No surprises, no deviations.

The power of deterministic workflows in the Screenplay pattern comes from their declarative nature. You describe what should happen at a business-logic level, and the underlying interactions handle the technical implementation. Consider an e-commerce checkout workflow:

import { Task } from '@serenity-js/core';
import { Navigate, Click, Enter } from '@serenity-js/web';
import { Ensure, equals } from '@serenity-js/assertions';

const CompleteCheckout = {
  asGuest: () =>
    Task.where(`#actor completes checkout as guest`,
      Navigate.to('/checkout'),
      Click.on('#guest-checkout'),
      Enter.theValue('john@example.com').into('#email'),
      Click.on('#continue-to-shipping'),
      FillShippingDetails.withAddress({
        street: '123 Main St',
        city: 'Portland',
        zip: '97201'
      }),
      Click.on('#continue-to-payment'),
      FillPaymentDetails.withTestCard(),
      Click.on('#place-order'),
      Ensure.that(OrderConfirmation.message(), equals('Order placed successfully'))
    ),
};

// Usage in a test
await james.attemptsTo(
  CompleteCheckout.asGuest()
);

Notice how readable this becomes. The test describes user behavior, not implementation details. Each sub-task (FillShippingDetails, FillPaymentDetails) encapsulates complexity. If the shipping form changes from a single page to a multi-step wizard, you update the FillShippingDetails task, and every workflow using it adapts automatically. This is maintenance efficiency that actually works in practice.

Deterministic workflows excel in regression testing, smoke tests, and scenarios where compliance or audit trails matter. When you need to prove that a critical business process works identically every time, determinism is non-negotiable. The challenge is that real users don't behave deterministically. They pause, they make mistakes, they take unexpected paths. This is where the pattern's flexibility becomes critical—because the same actor can execute both deterministic and non-deterministic workflows using the same interaction vocabulary.

Non-Deterministic AI-Driven Workflows: Adaptive Intelligence

Non-deterministic workflows represent the frontier of intelligent automation. Instead of following a rigid script, these workflows make decisions based on context, application state, or even AI-powered reasoning. The Actor-Interaction model accommodates this beautifully because actors can query application state through Questions and use that information to determine their next action. This is where automation transcends scripted testing and becomes truly intelligent.

The simplest form of non-deterministic workflow involves conditional logic based on application state. An actor might check whether a modal is visible and close it, or skip a step if data is already populated. More sophisticated implementations use AI models to interpret page content, decide which elements to interact with, or even generate test data that matches the application's current context. The key insight is that the interaction vocabulary remains consistent—Tasks, Actions, and Questions—but the orchestration becomes dynamic.

Here's an example of a workflow that adapts to application state:

import { Task, Question, Actor } from '@serenity-js/core';
import { Click, isVisible } from '@serenity-js/web';
import { Check } from '@serenity-js/assertions';

const HandlePopups = {
  ifPresent: () =>
    Task.where(`#actor handles any popups that appear`,
      Check.whether(NewsletterPopup.isVisible(), isVisible())
        .andIfSo(
          Click.on(NewsletterPopup.closeButton())
        ),
      Check.whether(CookieConsent.isVisible(), isVisible())
        .andIfSo(
          Click.on(CookieConsent.acceptButton())
        ),
      Check.whether(PromotionBanner.isVisible(), isVisible())
        .andIfSo(
          Click.on(PromotionBanner.dismissButton())
        ),
    ),
};

// AI-enhanced example using GPT to interpret page context
const DecideNextAction = {
  basedOnPageContext: () =>
    Task.where(`#actor decides next action using AI`,
      async (actor: Actor) => {
        const pageContent = await actor.answer(CurrentPage.textContent());
        const userGoal = await actor.answer(TakeNotes.note('current-goal'));
        
        const aiDecision = await callOpenAI({
          prompt: `Given this page content: "${pageContent}"
                   And this user goal: "${userGoal}"
                   What should the user do next? Options: continue, retry, skip, abort`,
          model: 'gpt-4'
        });
        
        switch(aiDecision.action) {
          case 'continue':
            return actor.attemptsTo(ProceedToNextStep());
          case 'retry':
            return actor.attemptsTo(RetryCurrentStep());
          case 'skip':
            return actor.attemptsTo(SkipToAlternativePath());
          case 'abort':
            return actor.attemptsTo(CancelWorkflow());
        }
      }
    ),
};

This AI-enhanced approach opens powerful possibilities. Visual AI can identify elements when selectors fail. Language models can generate contextually appropriate test data. Reinforcement learning can optimize test paths over time. But here's the brutal truth: AI-driven workflows are harder to debug, more expensive to run (API costs add up), and can produce flaky results if not carefully constrained. You need fallbacks, timeouts, and clear decision boundaries.

The real art is knowing when to use non-deterministic workflows. They shine in exploratory testing, handling unpredictable third-party integrations, testing recommendation engines or personalization features, and scenarios where user behavior varies widely. For everything else, stick with deterministic workflows. Complexity is a tax you pay in maintenance, and you should only pay it when the value clearly exceeds the cost.

Composing Workflows: From Tasks to Orchestrated Journeys

The true power of the Actor-Interaction model emerges when you compose simple Tasks into complex workflows. This is where the pattern moves beyond basic automation and becomes a language for describing user journeys. A workflow is a sequence of Tasks orchestrated to accomplish a high-level goal—like "complete onboarding" or "process a refund." These workflows can nest arbitrarily deep, mix deterministic and non-deterministic elements, and remain readable because each level maintains its abstraction.

Consider a realistic scenario: testing a multi-step application submission where some steps are mandatory, some are conditional, and the system might present different flows based on user data. With the Screenplay pattern, you model this naturally:

import { Task, Actor, Duration } from '@serenity-js/core';
import { Wait, isVisible } from '@serenity-js/web';

const SubmitLoanApplication = {
  withApplicantDetails: (applicantData: ApplicantData) =>
    Task.where(`#actor submits loan application`,
      Navigate.to('/apply'),
      FillPersonalInformation.with(applicantData.personal),
      FillEmploymentInformation.with(applicantData.employment),
      
      // Conditional: income verification only for certain employment types
      Check.whether(
        applicantData.employment.type, 
        equals('self-employed')
      ).andIfSo(
        UploadIncomeVerification.documents(applicantData.documents)
      ),
      
      SelectLoanAmount.of(applicantData.loanAmount),
      ReviewApplication.andConfirmAccuracy(),
      
      // AI-driven: handle dynamic verification flows
      HandleVerificationSteps.adaptively(),
      
      SubmitApplication.andWait(),
      
      // Deterministic validation
      Ensure.that(
        ApplicationStatus.currentState(),
        equals('submitted')
      )
    ),
};

const HandleVerificationSteps = {
  adaptively: () =>
    Task.where(`#actor handles verification steps`,
      async (actor: Actor) => {
        let verificationComplete = false;
        let attempts = 0;
        const maxAttempts = 5;
        
        while (!verificationComplete && attempts < maxAttempts) {
          const currentStep = await actor.answer(
            VerificationFlow.currentStepType()
          );
          
          switch(currentStep) {
            case 'identity':
              await actor.attemptsTo(VerifyIdentity.withDocuments());
              break;
            case 'income':
              await actor.attemptsTo(VerifyIncome.withTaxReturns());
              break;
            case 'credit':
              await actor.attemptsTo(AuthorizeCreditCheck());
              break;
            case 'complete':
              verificationComplete = true;
              break;
            case 'error':
              await actor.attemptsTo(ResolveVerificationError());
              break;
          }
          
          attempts++;
          await actor.attemptsTo(
            Wait.for(Duration.ofMilliseconds(500))
          );
        }
        
        if (!verificationComplete) {
          throw new Error('Verification flow did not complete');
        }
      }
    ),
};

This example demonstrates several critical patterns. First, business logic lives in the workflow composition, not scattered across page objects or test files. Second, conditional behavior is explicit and testable. Third, the workflow handles uncertainty (the dynamic verification steps) without becoming unreadable. Fourth, error handling is built into the flow—if verification doesn't complete after reasonable attempts, the workflow fails gracefully.

When composing workflows, follow these principles: keep each Task focused on a single responsibility; use Questions to query state, not to drive complex logic; make workflows self-documenting through naming; and always consider the failure modes. A well-composed workflow reads like a specification of user behavior and serves as living documentation of how the system should work.

Balancing Predictability and Adaptability: Practical Strategies

The hardest part of implementing the Actor-Interaction model isn't the technical mechanics—it's deciding where to draw the line between deterministic and non-deterministic workflows. Make everything deterministic, and your automation breaks when reality introduces variation. Make everything AI-driven, and you sacrifice reproducibility, increase costs, and create debugging nightmares. The sweet spot lies in strategic hybrid approaches.

Start with the 80/20 principle: 80% of your workflows should be deterministic, covering happy paths and critical business processes. These are your regression suite foundation—fast, reliable, and cheap to run. The remaining 20% can be non-deterministic, targeting edge cases, exploratory scenarios, and integrations with unpredictable external systems. Within that 20%, use a progressive enhancement approach: start with conditional logic based on application state (low complexity, high reliability), then add simple ML models for classification or prediction (medium complexity, medium reliability), and only use generative AI for truly complex decision-making (high complexity, requires careful constraints).

Here's a practical strategy for hybrid workflows:

import { Task, Actor, notes } from '@serenity-js/core';

const ProcessUserOnboarding = {
  withFlexibility: (userData: UserData) =>
    Task.where(`#actor completes onboarding with adaptive handling`,
      // Deterministic: always start the same way
      Navigate.to('/onboarding'),
      FillBasicProfile.with(userData.profile),
      
      // Adaptive: handle variable welcome flow
      HandleWelcomeFlow.intelligently(),
      
      // Deterministic: core feature setup
      SelectFeaturePreferences.from(userData.preferences),
      
      // Adaptive: personalization might present different options
      ConfigurePersonalization.basedOnAvailableOptions(),
      
      // Deterministic: always validate completion
      Ensure.that(OnboardingStatus.isComplete(), equals(true))
    ),
};

const HandleWelcomeFlow = {
  intelligently: () =>
    Task.where(`#actor handles welcome flow`,
      // First, try deterministic approach
      Check.whether(StandardWelcomeModal.isVisible(), isVisible())
        .andIfSo(
          Click.on(StandardWelcomeModal.continueButton())
        )
        .otherwise(
          // Fall back to adaptive approach
          AdaptiveModalHandler.processUnknownModal()
        )
    ),
};

const AdaptiveModalHandler = {
  processUnknownModal: () =>
    Task.where(`#actor handles unknown modal`,
      async (actor: Actor) => {
        // Use visual AI to identify modal type
        const screenshot = await actor.answer(CurrentPage.screenshot());
        const modalType = await classifyModalType(screenshot);
        
        switch(modalType) {
          case 'welcome':
            await actor.attemptsTo(DismissWelcomeModal());
            break;
          case 'terms':
            await actor.attemptsTo(AcceptTerms());
            break;
          case 'survey':
            await actor.attemptsTo(SkipSurvey());
            break;
          default:
            // Log for investigation but don't fail
            await actor.attemptsTo(
              TakeNotes.note('unknown-modal', modalType)
            );
        }
      }
    ),
};

// Helper for ML classification (example using a hypothetical service)
async function classifyModalType(screenshot: Buffer): Promise<string> {
  const response = await fetch('https://ml-service.example.com/classify', {
    method: 'POST',
    body: JSON.stringify({
      image: screenshot.toString('base64'),
      categories: ['welcome', 'terms', 'survey', 'unknown']
    })
  });
  
  const result = await response.json();
  return result.prediction;
}

Another crucial strategy is to use deterministic workflows for validation, even when the execution path is non-deterministic. Your actor might take different routes to reach a goal, but the assertions about the final state should be consistent. This gives you the best of both worlds: flexibility in execution, confidence in outcomes.

Monitor your non-deterministic workflows carefully. Track decision frequencies, execution times, and failure patterns. If an AI-driven decision consistently chooses the same path, replace it with a deterministic check—you're paying for complexity you don't need. If a workflow becomes flaky, add more deterministic guardrails. The goal isn't to use AI because it's exciting; it's to use the right tool for each specific challenge.

The 80/20 Rule: Focus on High-Impact Patterns

When implementing the Actor-Interaction model, 20% of the patterns you learn will deliver 80% of the value. Focus your energy on mastering these core concepts, and you'll be productive immediately. Here's what actually matters in real-world practice.

First, master Task composition. Creating clean, reusable Tasks is the single most valuable skill. A Task should represent a meaningful user activity, use descriptive naming that non-technical stakeholders understand, and compose smaller Tasks or Actions without leaking implementation details. Practice extracting Tasks from your test scripts until this becomes intuitive. Second, understand Question patterns. Questions are your interface to application state. Learn to write Questions that return strongly-typed data, Questions that wait for conditions to be true, and Questions that query multiple sources (UI, API, database) consistently. These two skills—composing Tasks and querying with Questions—will cover the vast majority of your automation scenarios.

Third, internalize the concept of Abilities as dependency injection. Instead of hard-coding browser instances or API clients into your interactions, inject them as Abilities that actors possess. This makes your code testable, portable across different execution environments, and easier to mock for unit testing. Here's the minimal pattern you need to know:

import { Ability, Actor } from '@serenity-js/core';

// Define a custom ability
class AccessDatabase extends Ability {
  static using(connectionString: string) {
    return new AccessDatabase(connectionString);
  }
  
  private constructor(private connection: string) {
    super();
  }
  
  async query(sql: string): Promise<any[]> {
    // Implementation details hidden
    return executeQuery(this.connection, sql);
  }
}

// Actor uses the ability
const admin = Actor.named('Admin').whoCan(
  BrowseTheWeb.using(browser),
  AccessDatabase.using('postgresql://localhost/testdb')
);

// Task uses the ability without knowing implementation
const VerifyUserInDatabase = {
  exists: (email: string) =>
    Task.where(`#actor verifies user exists in database`,
      async (actor: Actor) => {
        const db = actor.abilityTo(AccessDatabase);
        const results = await db.query(
          `SELECT * FROM users WHERE email = '${email}'`
        );
        
        if (results.length === 0) {
          throw new Error(`User ${email} not found in database`);
        }
      }
    ),
};

The remaining 80% of the pattern—custom Interactions, advanced error handling, reporting integrations, parallel execution strategies—are important but not essential for day-one productivity. You'll learn them when you need them. By focusing on these three core concepts, you can refactor an entire legacy test suite into the Screenplay pattern within weeks and immediately see benefits in maintainability and readability.

Key Takeaways: Your Roadmap to Implementation

If you take nothing else from this article, internalize these five action items. They represent the concrete steps for successfully implementing the Actor-Interaction model in your organization, distilled from real-world experience across multiple teams and codebases.

Action 1: Start with one workflow, not a framework. Don't attempt to refactor your entire test suite at once. Choose one critical user journey—something important but not so complex that it overwhelms. Implement it using the Screenplay pattern: create the necessary Tasks, Actions, and Questions. Get it working, running in your CI pipeline, providing value. This proof of concept is essential for building team buy-in and understanding the pattern's implications in your specific context.

Action 2: Establish a shared Task library. Create a dedicated module or package for reusable Tasks that represent common user activities in your application. Document naming conventions: use present-tense action verbs, make Tasks read like requirements, include the business capability being tested. This library becomes your team's shared vocabulary for automation. When product managers and QA engineers can read test code and understand it without translation, you've achieved something powerful.

Action 3: Keep deterministic and non-deterministic workflows separated. Create different test suites or clearly marked test categories. Your deterministic workflows run on every commit, giving fast feedback. Your non-deterministic workflows run less frequently—maybe nightly or weekly—and are explicitly marked as exploratory or adaptive. This separation manages expectations about stability, execution time, and maintenance requirements. Don't mix them in the same test file; the cognitive overhead of switching between mindsets will slow your team down.

Action 4: Implement defensive Questions. Every Question should handle the scenario where the information it's querying doesn't exist or isn't ready yet. Use explicit waits, return optional types, provide sensible defaults. A Question that throws cryptic errors when an element isn't found creates debugging friction. A Question that waits for the element to appear or returns a clear "not found" state makes workflows resilient. This small discipline prevents 90% of flaky tests.

Action 5: Measure and iterate. Track metrics that matter: test execution time, failure rate, time to fix failing tests, and team velocity in writing new tests. The Actor-Interaction model should improve all of these over time. If execution time increases significantly, you're probably over-using AI or making excessive API calls. If failure rates remain high, your Tasks might be too complex or your Questions too brittle. If maintenance time doesn't decrease within a few months, something in your implementation isn't aligning with the pattern's principles. Use these signals to course-correct early.

Analogies and Mental Models: Making It Stick

Understanding the Actor-Interaction model intellectually is one thing; internalizing it so you think naturally in its terms is another. These analogies have helped teams across different organizations grasp the pattern's essence and recall it when designing new automation.

Think of the Actor as a method actor preparing for a role. Before stepping on stage, the actor acquires abilities: learning to fence for a swashbuckler role, mastering a dialect for a period piece, training in dance for a musical. These abilities don't dictate what scenes the actor performs; they enable the actor to perform whatever the script demands. Similarly, your test actors gain Abilities (browsing the web, calling APIs, accessing databases) that enable them to perform any Task the workflow requires. The abilities are capabilities, not instructions.

Consider Tasks as recipes and Actions as cooking techniques. A recipe (Task) like "Bake chocolate chip cookies" composes multiple techniques (Actions): "cream butter and sugar," "fold in flour," "shape dough." If you improve your technique for "folding in flour," every recipe using that technique benefits. You can also create new recipes by recombining techniques in novel ways. This is exactly how Tasks and Actions relate—Tasks compose Actions, and improving an Action improves all dependent Tasks automatically. The key insight is that recipes operate at the level of "what you want to make," while techniques operate at the level of "how you manipulate ingredients."

Picture Questions as a scientist taking measurements. A scientist doesn't ask a thermometer "what's the temperature?"—they read it, then decide what to do based on the reading. Questions in the Screenplay pattern work identically. They're instruments that measure application state without judgment. The workflow interprets those measurements and decides on actions. This separation is crucial: Questions never contain business logic, never make decisions, never trigger side effects. They observe and report, nothing more. When you're tempted to add conditional logic to a Question, that's a sign you need a Task that uses the Question's result to make decisions.

Think of deterministic workflows as highway driving and non-deterministic workflows as city driving. On the highway, you follow a predictable route at consistent speed with clear markers. In the city, you adapt to traffic, construction, pedestrians, and changing signals. Sometimes you take your planned route; sometimes you take a detour. Both types of driving are necessary, but they require different skills and mindsets. Your automation should work the same way: use deterministic workflows for well-paved scenarios, use non-deterministic workflows when you need to adapt to uncertainty. Just as you wouldn't take surface streets for a long-distance trip (too slow) or try to navigate a downtown on a highway (impossible), you shouldn't use the wrong workflow type for the scenario at hand.

Finally, consider the Actor-Interaction model as a musical ensemble. Each musician (Actor) has instruments they can play (Abilities), knows individual parts (Actions and Questions), and performs pieces (Tasks) that combine those parts into meaningful music. A symphony (Workflow) coordinates multiple pieces, sometimes playing in sequence, sometimes in harmony, adapting dynamics based on the conductor's interpretation (AI-driven decisions) while maintaining the composition's structure (deterministic flow). When one musician improves their technique on a passage, every performance of every piece containing that passage improves. The ensemble can sight-read new compositions because the vocabulary of musical notation is shared. This multilayered analogy captures the composability, reusability, and flexibility that makes the pattern powerful.

Conclusion: From Pattern to Practice

The Actor-Interaction model isn't just another testing framework or architectural trend. It's a fundamental rethinking of how we model user behavior in automated systems. By organizing automation around what users do rather than how applications are structured, we create code that's resilient to change, readable by non-technical stakeholders, and capable of evolving from rigid scripts into intelligent, adaptive workflows. The pattern's elegance lies in its simplicity: actors with abilities perform tasks composed of actions and answer questions about state. That's the entire model, yet it scales from simple login flows to complex, AI-driven test orchestration.

The path to mastery isn't learning every advanced feature or memorizing complex APIs. It's internalizing the core principles—composability, separation of concerns, explicit behavior modeling—and applying them consistently. Start small. Refactor one critical workflow. Build your Task library gradually. Experiment with hybrid approaches that combine deterministic reliability with adaptive intelligence. Measure your results and iterate based on what you learn. The teams I've seen succeed with this pattern didn't try to boil the ocean; they demonstrated value incrementally and let success breed adoption.

What makes this moment particularly exciting is the convergence of this architectural pattern with AI capabilities. The Screenplay pattern was designed years before GPT-4, before visual AI, before the current wave of intelligent automation tools. Yet its architecture accommodates AI naturally because it always separated what we want to accomplish from how we accomplish it. That separation creates the space for AI to contribute without requiring architectural overhauls. We're at the beginning of this journey, not the end. As AI capabilities improve, workflows that seemed impossibly complex—testing personalized recommendations, validating machine learning outputs, adapting to novel application states—become tractable using the same Actor-Interaction vocabulary you're learning today.

The future of UI automation isn't deterministic or non-deterministic—it's knowing when to use each approach, how to compose them effectively, and how to maintain human oversight of increasingly capable systems. The Actor-Interaction model gives you the foundation for that future. What you build on top of it is limited only by your creativity and your commitment to creating automation that serves users, teams, and business goals rather than just validating that buttons still click. Now go build something remarkable.

References and Further Reading