feat(js): implement intersection observer lazy loading for adsense
- Add per-slot lazy loading with Intersection Observer API - Implement fill detection via MutationObserver and data-ad-status - Add configurable rootMargin and fillTimeout from database - Generate dynamic CSS based on lazy_loading_enabled setting - Add legacy mode fallback for browsers without IO support - Include backup of previous implementation (adsense-loader.legacy.js) - Add OpenSpec documentation with test plan (72 tests verified) Schema changes: - Add lazy_loading_enabled (boolean, default: true) - Add lazy_rootmargin (select: 0-500px, default: 200) - Add lazy_fill_timeout (select: 3000-10000ms, default: 5000) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
# Especificacion: AdSense Lazy Loading
|
||||
|
||||
## Purpose
|
||||
|
||||
Define el comportamiento del sistema de carga diferida de anuncios AdSense usando Intersection Observer para cargar ads individualmente cuando entran al viewport, ocultando slots que no reciben contenido.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Carga Individual por Visibilidad
|
||||
|
||||
The system MUST load each AdSense ad slot individually when it enters the viewport, NOT all at once.
|
||||
|
||||
#### Scenario: Slot entra al viewport por primera vez
|
||||
|
||||
- **WHEN** un elemento `.roi-ad-slot[data-ad-lazy="true"]` entra al viewport (considerando rootMargin)
|
||||
- **THEN** el sistema DEBE ejecutar `adsbygoogle.push({})` SOLO para ese slot
|
||||
- **AND** el sistema DEBE marcar el slot como "activado" para no procesarlo de nuevo
|
||||
- **AND** el sistema DEBE observar el `<ins>` interno para detectar contenido
|
||||
|
||||
#### Scenario: Multiples slots en viewport inicial
|
||||
|
||||
- **GIVEN** la pagina tiene 3 slots visibles en el viewport inicial
|
||||
- **WHEN** la pagina termina de cargar
|
||||
- **THEN** el sistema DEBE activar los 3 slots en orden DOM (sin delay entre ellos)
|
||||
- **AND** la activacion es sincrona: push() → siguiente push() inmediatamente
|
||||
- **AND** el sistema NO DEBE activar slots que estan fuera del viewport
|
||||
|
||||
**Clarificacion:** "Secuencial" significa en orden DOM, uno tras otro sin delay artificial. NO hay setTimeout entre activaciones. El Intersection Observer dispara callbacks para todos los elementos visibles en el mismo frame.
|
||||
|
||||
#### Scenario: Usuario hace scroll rapido
|
||||
|
||||
- **GIVEN** el usuario hace scroll rapido pasando varios slots
|
||||
- **WHEN** los slots entran y salen del viewport rapidamente
|
||||
- **THEN** el sistema DEBE activar cada slot que entre al viewport
|
||||
- **AND** el sistema NO DEBE cancelar la activacion si el slot sale del viewport
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Biblioteca Cargada Una Sola Vez
|
||||
|
||||
The system MUST load the `adsbygoogle.js` library only once, when the first slot is activated.
|
||||
|
||||
#### Scenario: Primer slot activado
|
||||
|
||||
- **GIVEN** la biblioteca `adsbygoogle.js` NO ha sido cargada
|
||||
- **WHEN** el primer slot entra al viewport
|
||||
- **THEN** el sistema DEBE cargar la biblioteca
|
||||
- **AND** el sistema DEBE esperar a que la biblioteca cargue (onload callback)
|
||||
- **AND** ENTONCES ejecutar el push para ese slot
|
||||
|
||||
#### Scenario: Slots subsecuentes
|
||||
|
||||
- **GIVEN** la biblioteca `adsbygoogle.js` YA fue cargada
|
||||
- **WHEN** otro slot entra al viewport
|
||||
- **THEN** el sistema DEBE ejecutar el push inmediatamente
|
||||
- **AND** el sistema NO DEBE intentar cargar la biblioteca de nuevo
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Slots Ocultos por Defecto
|
||||
|
||||
The system MUST hide ad slots by default and show them only when they have content.
|
||||
|
||||
#### Scenario: Slot en estado inicial
|
||||
|
||||
- **WHEN** la pagina renderiza un `.roi-ad-slot[data-ad-lazy="true"]`
|
||||
- **THEN** el slot DEBE tener `display: none` via CSS dinamico
|
||||
- **AND** el slot NO DEBE ocupar espacio en el layout
|
||||
|
||||
#### Scenario: Slot recibe contenido de Google
|
||||
|
||||
- **GIVEN** un slot fue activado con push()
|
||||
- **WHEN** Google inyecta contenido dentro del `<ins class="adsbygoogle">`
|
||||
- **THEN** el sistema DEBE agregar clase `roi-ad-filled` al slot
|
||||
- **AND** el slot DEBE hacerse visible (`display: block`)
|
||||
|
||||
#### Scenario: Slot NO recibe contenido (timeout)
|
||||
|
||||
- **GIVEN** un slot fue activado con push()
|
||||
- **WHEN** pasa el tiempo configurado en `lazy_fill_timeout` sin que Google inyecte contenido
|
||||
- **THEN** el sistema DEBE agregar clase `roi-ad-empty` al slot
|
||||
- **AND** el slot DEBE permanecer oculto
|
||||
- **AND** el sistema DEBE dejar de observar ese slot
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Pre-carga con rootMargin
|
||||
|
||||
The system MUST pre-load ads before they enter the visible viewport to ensure smooth UX.
|
||||
|
||||
#### Scenario: Configuracion de rootMargin
|
||||
|
||||
- **WHEN** se inicializa el Intersection Observer
|
||||
- **THEN** DEBE usar el valor de `lazy_rootmargin` desde configuracion
|
||||
- **AND** el formato DEBE ser `'{value}px 0px'`
|
||||
|
||||
#### Scenario: Slot dentro del rootMargin
|
||||
|
||||
- **GIVEN** un slot esta 150px debajo del viewport visible
|
||||
- **AND** `lazy_rootmargin` es 200
|
||||
- **WHEN** el Intersection Observer evalua visibilidad
|
||||
- **THEN** el slot DEBE considerarse "visible" y activarse
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Deteccion de Contenido con Criterios Concretos
|
||||
|
||||
The system MUST use specific criteria to determine when an ad slot has been filled.
|
||||
|
||||
#### Scenario: Google agrega atributo data-ad-status="filled"
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **WHEN** Google agrega `data-ad-status="filled"` al `<ins>`
|
||||
- **THEN** el sistema DEBE marcar inmediatamente como `roi-ad-filled`
|
||||
- **AND** el sistema DEBE desconectar observadores de ese slot
|
||||
|
||||
#### Scenario: Google agrega atributo data-ad-status="unfilled"
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **WHEN** Google agrega `data-ad-status="unfilled"` al `<ins>`
|
||||
- **THEN** el sistema DEBE marcar inmediatamente como `roi-ad-empty`
|
||||
- **AND** el sistema DEBE desconectar observadores de ese slot
|
||||
|
||||
#### Scenario: Fallback - Google inyecta iframe sin atributo
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **AND** el `<ins>` NO tiene atributo `data-ad-status`
|
||||
- **WHEN** Google agrega un `<iframe>` dentro del `<ins>`
|
||||
- **THEN** el sistema DEBE marcar como `roi-ad-filled`
|
||||
|
||||
#### Scenario: Fallback - Google agrega div con id
|
||||
|
||||
- **GIVEN** un slot fue activado
|
||||
- **AND** el `<ins>` NO tiene atributo `data-ad-status`
|
||||
- **WHEN** Google agrega un `<div id="...">` dentro del `<ins>`
|
||||
- **THEN** el sistema DEBE marcar como `roi-ad-filled`
|
||||
|
||||
#### Scenario: Limpieza de observadores
|
||||
|
||||
- **GIVEN** un slot fue marcado como `roi-ad-filled` o `roi-ad-empty`
|
||||
- **WHEN** el estado final es determinado
|
||||
- **THEN** el sistema DEBE desconectar el MutationObserver de ese slot
|
||||
- **AND** el sistema DEBE desconectar el IntersectionObserver de ese slot
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Manejo de Errores de Red
|
||||
|
||||
The system MUST handle network errors when loading the AdSense library.
|
||||
|
||||
#### Scenario: Error de carga de biblioteca - primer intento
|
||||
|
||||
- **GIVEN** el sistema intenta cargar `adsbygoogle.js`
|
||||
- **WHEN** la carga falla (onerror)
|
||||
- **THEN** el sistema DEBE esperar 2 segundos
|
||||
- **AND** el sistema DEBE reintentar la carga UNA vez
|
||||
|
||||
#### Scenario: Error de carga de biblioteca - segundo intento fallido
|
||||
|
||||
- **GIVEN** el primer intento de carga fallo
|
||||
- **AND** el segundo intento tambien falla
|
||||
- **WHEN** el onerror se dispara por segunda vez
|
||||
- **THEN** el sistema DEBE marcar TODOS los slots como `roi-ad-error`
|
||||
- **AND** el sistema DEBE registrar error en consola si debug habilitado
|
||||
- **AND** el sistema NO DEBE intentar mas recargas
|
||||
|
||||
#### Scenario: Slots permanecen ocultos tras error
|
||||
|
||||
- **GIVEN** la biblioteca fallo en cargar
|
||||
- **WHEN** los slots tienen clase `roi-ad-error`
|
||||
- **THEN** los slots DEBEN permanecer ocultos
|
||||
- **AND** NO DEBEN mostrar espacios vacios en la pagina
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fallback para Navegadores Sin Soporte
|
||||
|
||||
The system MUST provide fallback for browsers without Intersection Observer support.
|
||||
|
||||
#### Scenario: Navegador sin Intersection Observer
|
||||
|
||||
- **GIVEN** `window.IntersectionObserver` es undefined
|
||||
- **WHEN** el script se inicializa
|
||||
- **THEN** el sistema DEBE usar el modo legacy (cargar todos despues de interaccion/timeout)
|
||||
- **AND** el sistema DEBE registrar un mensaje de debug indicando fallback
|
||||
|
||||
#### Scenario: Navegador con soporte parcial
|
||||
|
||||
- **GIVEN** el navegador soporta Intersection Observer pero no MutationObserver
|
||||
- **WHEN** el script se inicializa
|
||||
- **THEN** el sistema DEBE usar Intersection Observer para activacion
|
||||
- **AND** el sistema DEBE usar timeout fijo para determinar fill (sin deteccion dinamica)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Compatibilidad con Ads Dinamicos
|
||||
|
||||
The system MUST support ads injected dynamically after page load.
|
||||
|
||||
#### Scenario: Contenido cargado via AJAX
|
||||
|
||||
- **GIVEN** la pagina carga contenido adicional via AJAX con nuevos slots
|
||||
- **WHEN** el evento `roi-adsense-activate` es disparado
|
||||
- **THEN** el sistema DEBE buscar nuevos slots `.roi-ad-slot[data-ad-lazy="true"]` no observados
|
||||
- **AND** el sistema DEBE agregarlos al Intersection Observer
|
||||
|
||||
#### Scenario: Infinite scroll
|
||||
|
||||
- **GIVEN** la pagina implementa infinite scroll
|
||||
- **WHEN** nuevos slots son agregados al DOM
|
||||
- **THEN** el sistema DEBE detectarlos automaticamente (MutationObserver en body)
|
||||
- **OR** esperar evento `roi-adsense-activate` para procesarlos
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Configuracion desde Base de Datos
|
||||
|
||||
The system MUST read configuration from database via wp_localize_script, NOT from hardcoded values.
|
||||
|
||||
#### Scenario: Configuracion disponible en JS
|
||||
|
||||
- **WHEN** el script `adsense-loader.js` se ejecuta
|
||||
- **THEN** DEBE leer configuracion de `window.roiAdsenseConfig`
|
||||
- **AND** los valores DEBEN incluir:
|
||||
- `lazyEnabled` (boolean) - desde campo `lazy_loading_enabled`
|
||||
- `rootMargin` (string) - desde campo `lazy_rootmargin` + 'px 0px'
|
||||
- `fillTimeout` (number) - desde campo `lazy_fill_timeout`
|
||||
- `debug` (boolean) - desde WP_DEBUG
|
||||
|
||||
#### Scenario: Modo lazy deshabilitado
|
||||
|
||||
- **GIVEN** `roiAdsenseConfig.lazyEnabled` es false
|
||||
- **WHEN** el script se inicializa
|
||||
- **THEN** el sistema DEBE usar el modo legacy (cargar todos al inicio)
|
||||
- **AND** los slots DEBEN ser visibles por defecto (sin display:none)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: No Manipular Ads Cargados
|
||||
|
||||
The system MUST NOT remove, recycle, or manipulate ads after they are loaded.
|
||||
|
||||
#### Scenario: Usuario scrollea pasando un ad
|
||||
|
||||
- **GIVEN** un ad fue cargado y mostrado
|
||||
- **WHEN** el usuario scrollea y el ad sale del viewport
|
||||
- **THEN** el sistema NO DEBE remover el ad del DOM
|
||||
- **AND** el sistema NO DEBE ocultar el ad
|
||||
- **AND** el sistema NO DEBE intentar "reciclar" el slot
|
||||
|
||||
#### Scenario: Ad permanece en pagina
|
||||
|
||||
- **GIVEN** un ad fue cargado exitosamente
|
||||
- **WHEN** la sesion del usuario continua
|
||||
- **THEN** el ad DEBE permanecer en su posicion original
|
||||
- **AND** el ad DEBE mantener su contenido intacto
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Logging de Debug Condicional
|
||||
|
||||
The system MUST provide debug logging only when enabled via WP_DEBUG.
|
||||
|
||||
#### Scenario: Debug habilitado
|
||||
|
||||
- **GIVEN** `roiAdsenseConfig.debug` es true
|
||||
- **WHEN** ocurre cualquier evento significativo
|
||||
- **THEN** el sistema DEBE registrar en console.log con prefijo `[AdSense Lazy]`
|
||||
- **AND** los eventos incluyen: inicializacion, activacion de slot, deteccion de fill, timeout, error
|
||||
|
||||
#### Scenario: Debug deshabilitado
|
||||
|
||||
- **GIVEN** `roiAdsenseConfig.debug` es false
|
||||
- **WHEN** el script ejecuta
|
||||
- **THEN** el sistema NO DEBE generar output en consola
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CSS Generado Dinamicamente
|
||||
|
||||
The system MUST generate CSS via CSSGeneratorService, NOT static CSS files.
|
||||
|
||||
#### Scenario: Lazy loading habilitado
|
||||
|
||||
- **GIVEN** `lazy_loading_enabled` es true en BD
|
||||
- **WHEN** `AdsensePlacementRenderer` genera output
|
||||
- **THEN** DEBE usar `CSSGeneratorService` para generar:
|
||||
- `.roi-ad-slot { display: none }`
|
||||
- `.roi-ad-slot.roi-ad-filled { display: block }`
|
||||
- `.roi-ad-slot.roi-ad-empty { display: none }`
|
||||
|
||||
#### Scenario: Lazy loading deshabilitado
|
||||
|
||||
- **GIVEN** `lazy_loading_enabled` es false en BD
|
||||
- **WHEN** `AdsensePlacementRenderer` genera output
|
||||
- **THEN** NO DEBE agregar `display: none` a `.roi-ad-slot`
|
||||
- **AND** los slots DEBEN ser visibles por defecto
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Integracion con Schema JSON
|
||||
|
||||
The system MUST store lazy loading configuration in the existing adsense-placement.json schema.
|
||||
|
||||
#### Scenario: Campos en grupo behavior
|
||||
|
||||
- **WHEN** el schema `adsense-placement.json` es leido
|
||||
- **THEN** el grupo `behavior` DEBE contener:
|
||||
- `lazy_loading_enabled` (boolean, default: true)
|
||||
- `lazy_rootmargin` (select, default: "200")
|
||||
- `lazy_fill_timeout` (select, default: "5000")
|
||||
|
||||
#### Scenario: Sincronizacion a BD
|
||||
|
||||
- **WHEN** se ejecuta `wp roi-theme sync-component adsense-placement`
|
||||
- **THEN** los campos de lazy loading DEBEN crearse en BD
|
||||
- **AND** los valores default DEBEN aplicarse si no existen
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Accesibilidad de Slots Ocultos
|
||||
|
||||
The system MUST ensure hidden ad slots do not interfere with assistive technologies.
|
||||
|
||||
#### Scenario: Slot oculto no interfiere con lectores de pantalla
|
||||
|
||||
- **GIVEN** un slot tiene `display: none` (estado inicial o roi-ad-empty)
|
||||
- **WHEN** un lector de pantalla procesa la pagina
|
||||
- **THEN** el slot NO DEBE ser anunciado ni navegable
|
||||
- **AND** el contenido oculto NO DEBE aparecer en el arbol de accesibilidad
|
||||
|
||||
#### Scenario: Slot visible es accesible
|
||||
|
||||
- **GIVEN** un slot fue marcado como `roi-ad-filled`
|
||||
- **WHEN** el slot se hace visible (`display: block`)
|
||||
- **THEN** el contenido del ad DEBE ser accesible para lectores de pantalla
|
||||
- **AND** el iframe de Google conserva su propia accesibilidad
|
||||
|
||||
**Nota tecnica:** `display: none` automaticamente remueve elementos del arbol de accesibilidad. No se requiere `aria-hidden` adicional.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Interaccion con Cache
|
||||
|
||||
The system MUST document cache implications when lazy loading settings change.
|
||||
|
||||
#### Scenario: Cambio de configuracion requiere cache flush
|
||||
|
||||
- **GIVEN** `lazy_loading_enabled` cambia de true a false (o viceversa)
|
||||
- **WHEN** el administrador guarda la configuracion
|
||||
- **THEN** el FormBuilder DEBE mostrar aviso de que se requiere vaciar cache
|
||||
- **AND** el CSS dinamico cambiara en el proximo render sin cache
|
||||
|
||||
#### Scenario: Usuario con cache obsoleto
|
||||
|
||||
- **GIVEN** un usuario tiene HTML cacheado con `display: none` en slots
|
||||
- **AND** el admin deshabilito lazy loading
|
||||
- **WHEN** el usuario visita la pagina
|
||||
- **THEN** los slots permaneceran ocultos hasta que el cache expire
|
||||
- **AND** esto es comportamiento esperado (no es un bug)
|
||||
Reference in New Issue
Block a user