Class: Unmagic::Browser::Emulation

Inherits:
Object
  • Object
show all
Defined in:
lib/unmagic/browser/emulation.rb

Overview

Everything about how a page renders — colour scheme, viewport, device, locale, timezone, geolocation, media type — in one immutable value object.

It's the only place emulation lives, and it's set in exactly two places: passed to #open, or changed live with Session#emulate.

Anything left unset renders at a fixed default — a plain desktop viewport, light, screen media — rather than at whatever the machine underneath happens to prefer. That's what makes re-applying an older Emulation a clean revert rather than a patch, and what stops the same page rendering differently on a laptop and on CI.

Examples:

Unmagic::Browser::Emulation.new(color_scheme: :dark, device: "iPhone 13")

Constant Summary collapse

DEFAULT_VIEWPORT =

What a page looks like with nothing emulated. Applied for any viewport key the caller leaves out, so reverting is always total.

{
  width: 1280,
  height: 800,
  scale: 1,
  mobile: false,
  touch: false,
}.freeze
COLOR_SCHEMES =

What prefers-color-scheme can be told to report.

[:light, :dark, :no_preference].freeze
REDUCED_MOTIONS =

What prefers-reduced-motion can be told to report.

[:reduce, :no_preference].freeze
MEDIA_TYPES =

What the page can be told it's being rendered for.

[:screen, :print].freeze
DEFAULT_USER_AGENT =

Present as a current desktop Chrome rather than as a headless build: pages render the way a real visitor sees them, and modern-browser gates let us through.

"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " \
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(color_scheme: nil, reduced_motion: nil, viewport: nil, device: nil, user_agent: nil, locale: nil, timezone: nil, geolocation: nil, media: nil) ⇒ Emulation

Returns a new instance of Emulation.

Parameters:

  • color_scheme (Symbol, nil) (defaults to: nil)

    :light, :dark or :no_preference

  • reduced_motion (Symbol, nil) (defaults to: nil)

    :reduce or :no_preference

  • viewport (Hash, nil) (defaults to: nil)

    width:, height:, scale:, mobile:, touch:

  • device (String, nil) (defaults to: nil)

    a Puppeteer device name, e.g. "iPhone 13"

  • user_agent (String, nil) (defaults to: nil)

    overrides the device's own user agent

  • locale (String, nil) (defaults to: nil)

    a BCP 47 tag, sent as Accept-Language

  • timezone (String, nil) (defaults to: nil)

    an IANA zone, e.g. "Australia/Sydney"

  • geolocation (Hash, nil) (defaults to: nil)

    latitude:, longitude: and optionally accuracy:

  • media (Symbol, nil) (defaults to: nil)

    :screen or :print



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/unmagic/browser/emulation.rb', line 77

def initialize(color_scheme: nil, reduced_motion: nil, viewport: nil, device: nil,
  user_agent: nil, locale: nil, timezone: nil, geolocation: nil, media: nil)
  @color_scheme = validate_symbol(:color_scheme, color_scheme, COLOR_SCHEMES)
  @reduced_motion = validate_symbol(:reduced_motion, reduced_motion, REDUCED_MOTIONS)
  @media = validate_symbol(:media, media, MEDIA_TYPES)
  @viewport = viewport&.transform_keys(&:to_sym)&.freeze
  @device = device
  @user_agent = user_agent
  @locale = locale
  @timezone = timezone
  @geolocation = geolocation&.transform_keys(&:to_sym)&.freeze
  validate_viewport!
  validate_device!
  freeze
end

Instance Attribute Details

#color_schemeObject (readonly)

Returns the value of attribute color_scheme.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def color_scheme
  @color_scheme
end

#deviceObject (readonly)

Returns the value of attribute device.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def device
  @device
end

#geolocationObject (readonly)

Returns the value of attribute geolocation.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def geolocation
  @geolocation
end

#localeObject (readonly)

Returns the value of attribute locale.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def locale
  @locale
end

#mediaObject (readonly)

Returns the value of attribute media.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def media
  @media
end

#reduced_motionObject (readonly)

Returns the value of attribute reduced_motion.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def reduced_motion
  @reduced_motion
end

#timezoneObject (readonly)

Returns the value of attribute timezone.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def timezone
  @timezone
end

#user_agentObject (readonly)

Returns the value of attribute user_agent.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def user_agent
  @user_agent
end

#viewportObject (readonly)

Returns the value of attribute viewport.



65
66
67
# File 'lib/unmagic/browser/emulation.rb', line 65

def viewport
  @viewport
end

Class Method Details

.build(value) ⇒ Emulation

Coerce whatever a caller passed as emulate: into an Emulation.

Parameters:

  • value (Emulation, Hash, nil)

    an instance, a hash of knobs, or nothing

Returns:

Raises:

  • (ArgumentError)

    when value is neither



54
55
56
57
58
59
60
61
62
# File 'lib/unmagic/browser/emulation.rb', line 54

def build(value)
  case value
  when nil then new
  when Emulation then value
  when Hash then new(**value)
  else
    raise ArgumentError, "emulate: expects an Unmagic::Browser::Emulation or a Hash, got #{value.class}"
  end
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Returns whether the other emulation has the same settings.

Parameters:

  • other (Object)

Returns:

  • (Boolean)

    whether the other emulation has the same settings



126
127
128
# File 'lib/unmagic/browser/emulation.rb', line 126

def ==(other)
  other.is_a?(Emulation) && other.to_h == to_h
end

#apply(page) ⇒ void

This method returns an undefined value.

Push the whole configuration onto a live page. Every knob is sent every time — including the ones left unset, which are sent as their defaults — so applying an Emulation always lands the page in exactly this state, whatever it was in before.

Parameters:

  • page (Puppeteer::Page)


143
144
145
146
147
148
149
150
# File 'lib/unmagic/browser/emulation.rb', line 143

def apply(page)
  page.viewport = resolved_viewport
  page.user_agent = resolved_user_agent
  page.emulate_locale(@locale)
  page.emulate_timezone(@timezone)
  apply_geolocation(page)
  apply_media(page)
end

#default?Boolean

Returns whether every knob is at Chrome's default.

Returns:

  • (Boolean)

    whether every knob is at Chrome's default



120
121
122
# File 'lib/unmagic/browser/emulation.rb', line 120

def default?
  to_h.values.all?(&:nil?)
end

#hashInteger

Returns a hash of the settings, so an emulation works as a hash key.

Returns:

  • (Integer)

    a hash of the settings, so an emulation works as a hash key



132
133
134
# File 'lib/unmagic/browser/emulation.rb', line 132

def hash
  to_h.hash
end

#merge(other) ⇒ Emulation

A copy with +other+'s settings layered on top. Only keys the other side actually set are overridden, so emulate(color_scheme: :dark) on a session that's already emulating a phone keeps the phone.

Parameters:

Returns:



99
100
101
102
# File 'lib/unmagic/browser/emulation.rb', line 99

def merge(other)
  other = self.class.build(other)
  self.class.new(**to_h.merge(other.to_h) { |_key, mine, theirs| theirs.nil? ? mine : theirs })
end

#resolved_user_agentString

Returns the user agent sent to Chrome.

Returns:

  • (String)

    the user agent sent to Chrome



169
170
171
# File 'lib/unmagic/browser/emulation.rb', line 169

def resolved_user_agent
  @user_agent || puppeteer_device&.user_agent || DEFAULT_USER_AGENT
end

#resolved_viewportPuppeteer::Viewport

The viewport actually sent to Chrome: the device's, with any explicit viewport: keys layered over it, over the plain-desktop default.

Returns:

  • (Puppeteer::Viewport)


156
157
158
159
160
161
162
163
164
165
166
# File 'lib/unmagic/browser/emulation.rb', line 156

def resolved_viewport
  base = DEFAULT_VIEWPORT.merge(device_viewport).merge(@viewport || {})
  Puppeteer::Viewport.new(
    width: base[:width],
    height: base[:height],
    device_scale_factor: base[:scale] || 1,
    is_mobile: !!base[:mobile],
    has_touch: !!base[:touch],
    is_landscape: base[:width] > base[:height],
  )
end

#to_hHash

Returns the knobs, unset ones included as nil.

Returns:

  • (Hash)

    the knobs, unset ones included as nil



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/unmagic/browser/emulation.rb', line 105

def to_h
  {
    color_scheme: @color_scheme,
    reduced_motion: @reduced_motion,
    viewport: @viewport,
    device: @device,
    user_agent: @user_agent,
    locale: @locale,
    timezone: @timezone,
    geolocation: @geolocation,
    media: @media,
  }
end