Que Es el Patron Screenplay?

El patron Screenplay es una alternativa avanzada al Page Object Model. En vez de organizar tests alrededor de paginas, los organiza alrededor de actores que realizan tareas y hacen preguntas usando sus habilidades.

Piensa en ello como un guion de pelicula — describes lo que hacen los actores, no como se ve la UI.

POM vs Screenplay: Comparacion

Enfoque POM:

const loginPage = new LoginPage(page);
await loginPage.login('admin@test.com', 'secret');
const dashboard = new DashboardPage(page);
const name = await dashboard.getUserName();
expect(name).toBe('Admin');

Enfoque Screenplay:

await actor.attemptsTo(
  Login.withCredentials('admin@test.com', 'secret'),
  Navigate.to(Dashboard)
);
const name = await actor.asks(UserName.displayed());
expect(name).toBe('Admin');

La version Screenplay se lee mas como una descripcion en lenguaje natural del comportamiento del usuario.

Los Cuatro Bloques de Construccion

1. Actores

Un Actor representa un usuario del sistema con habilidades y contexto especificos.

class Actor {
  constructor(name) {
    this.name = name;
    this.abilities = new Map();
  }

  can(ability) {
    this.abilities.set(ability.constructor.name, ability);
    return this;
  }

  abilityTo(AbilityClass) {
    return this.abilities.get(AbilityClass.name);
  }

  async attemptsTo(...tasks) {
    for (const task of tasks) {
      await task.performAs(this);
    }
  }

  async asks(question) {
    return await question.answeredBy(this);
  }
}

const admin = new Actor('Admin')
  .can(new BrowseTheWeb(page))
  .can(new CallAnApi('https://api.example.com'));

2. Habilidades (Abilities)

Las Abilities definen lo que un Actor puede hacer — interactuar con un browser, llamar APIs, acceder bases de datos.

class BrowseTheWeb {
  constructor(page) {
    this.page = page;
  }

  static using(page) {
    return new BrowseTheWeb(page);
  }
}

class CallAnApi {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async get(path) {
    const response = await fetch(`${this.baseUrl}${path}`);
    return response.json();
  }

  async post(path, data) {
    const response = await fetch(`${this.baseUrl}${path}`, {
      method: 'POST',
      body: JSON.stringify(data),
      headers: { 'Content-Type': 'application/json' }
    });
    return response.json();
  }
}

3. Tareas (Tasks)

Las Tasks representan acciones de alto nivel del usuario. Son componibles — tareas complejas se construyen desde tareas mas simples.

class Login {
  constructor(email, password) {
    this.email = email;
    this.password = password;
  }

  static withCredentials(email, password) {
    return new Login(email, password);
  }

  async performAs(actor) {
    const page = actor.abilityTo(BrowseTheWeb).page;
    await page.goto('/login');
    await page.fill('[data-testid="email"]', this.email);
    await page.fill('[data-testid="password"]', this.password);
    await page.click('[data-testid="submit"]');
    await page.waitForURL('/dashboard');
  }
}

class AddProductToCart {
  constructor(productName) {
    this.productName = productName;
  }

  static called(productName) {
    return new AddProductToCart(productName);
  }

  async performAs(actor) {
    const page = actor.abilityTo(BrowseTheWeb).page;
    const card = page.locator(`.product:has-text("${this.productName}")`);
    await card.locator('.add-to-cart').click();
    await page.waitForSelector('.cart-updated');
  }
}

4. Preguntas (Questions)

Las Questions permiten a los actores consultar el estado del sistema.

class UserName {
  static displayed() {
    return new UserName();
  }

  async answeredBy(actor) {
    const page = actor.abilityTo(BrowseTheWeb).page;
    return await page.textContent('.user-name');
  }
}

class CartItemCount {
  static shown() {
    return new CartItemCount();
  }

  async answeredBy(actor) {
    const page = actor.abilityTo(BrowseTheWeb).page;
    const text = await page.textContent('.cart-count');
    return parseInt(text);
  }
}

Escribiendo Tests con Screenplay

test('cliente puede agregar items al carrito y hacer checkout', async ({ page }) => {
  const customer = new Actor('Customer')
    .can(BrowseTheWeb.using(page));

  await customer.attemptsTo(
    Login.withCredentials('customer@test.com', 'password'),
    AddProductToCart.called('Mouse Inalambrico'),
    AddProductToCart.called('Teclado USB')
  );

  const itemCount = await customer.asks(CartItemCount.shown());
  expect(itemCount).toBe(2);

  await customer.attemptsTo(
    ProceedToCheckout.now(),
    EnterShippingAddress.with(testAddress),
    PayWith.card(testCard)
  );

  const orderNumber = await customer.asks(OrderNumber.displayed());
  expect(orderNumber).toMatch(/^ORD-\d+$/);
});

Componiendo Tareas Complejas

Una de las fortalezas de Screenplay es la composicion de tareas:

class PlaceOrder {
  constructor(product, address, card) {
    this.product = product;
    this.address = address;
    this.card = card;
  }

  static for(product) {
    return {
      shippingTo: (address) => ({
        payingWith: (card) => new PlaceOrder(product, address, card)
      })
    };
  }

  async performAs(actor) {
    await actor.attemptsTo(
      AddProductToCart.called(this.product),
      ProceedToCheckout.now(),
      EnterShippingAddress.with(this.address),
      PayWith.card(this.card)
    );
  }
}

// Test de alto nivel
test('flujo de compra completo', async ({ page }) => {
  const customer = new Actor('Customer').can(BrowseTheWeb.using(page));
  await customer.attemptsTo(
    Login.withCredentials('customer@test.com', 'password'),
    PlaceOrder.for('Mouse Inalambrico').shippingTo(homeAddress).payingWith(visa)
  );
});

Screenplay vs POM: Cuando Elegir Cual

FactorPOMScreenplay
Curva de aprendizajeMenorMayor
Proyectos pequenosMejor opcionExcesivo
Flujos complejosPuede complicarseSobresale
Tests multi-sistemaLimitadoEncaje natural
Tamano de equipoCualquieraMediano-grande
LegibilidadBuenaExcelente
ReusabilidadNivel de paginaNivel de tarea

Elige POM Cuando:

  • Tu aplicacion tiene paginas claramente definidas
  • Tu equipo es pequeno o nuevo en automatizacion
  • Los tests son principalmente interacciones de una pagina
  • Necesitas comenzar rapidamente

Elige Screenplay Cuando:

  • Los flujos abarcan multiples paginas y sistemas
  • Necesitas maxima reusabilidad entre diferentes suites
  • Tu equipo es experimentado y puede manejar la complejidad
  • Los tests involucran multiples roles de usuario interactuando entre si

Serenity/JS: Screenplay en la Practica

Serenity/JS es un framework popular que implementa el patron Screenplay:

import { actorCalled } from '@serenity-js/core';
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright';
import { Navigate, Click, Enter } from '@serenity-js/web';

const actor = actorCalled('Customer')
  .whoCan(BrowseTheWebWithPlaywright.using(browser));

await actor.attemptsTo(
  Navigate.to('/login'),
  Enter.theValue('user@test.com').into('#email'),
  Enter.theValue('password').into('#password'),
  Click.on('#submit')
);

Ejercicio: Convierte POM a Screenplay

Toma el POM de e-commerce de la Leccion 8.8 y refactorizalo al patron Screenplay:

  1. Define un Actor con la ability BrowseTheWeb
  2. Convierte LoginPage.login() en un Task Login
  3. Convierte CartPage.addItem() en un Task AddToCart
  4. Crea Questions: CartTotal, ItemCount, OrderNumber
  5. Escribe un Task compuesto PlaceOrder
  6. Escribe 2 tests usando el enfoque Screenplay

Compara la legibilidad y flexibilidad de ambos enfoques.

Puntos Clave

  • Screenplay organiza tests alrededor de Actors, Tasks, Questions y Abilities
  • Los tests se leen como descripciones en lenguaje natural
  • Las Tasks son componibles — flujos complejos se construyen desde tareas simples
  • Screenplay sobresale en proyectos complejos multi-sistema
  • POM es mas simple y mejor para aplicaciones web directas
  • Ambos patrones pueden coexistir en el mismo proyecto