Class: Tuile::FakeScreen

Inherits:
Screen
  • Object
show all
Defined in:
lib/tuile/fake_screen.rb,
sig/tuile.rbs

Overview

Testing only — a screen which doesn't paint anything, so the TTY running the tests is not painted over. It runs no event loop, so Screen#check_locked admits the thread that called Screen.fake: a spec mutating the UI from a spawned thread raises, exactly as an app would.

Intended for unit-testing individual components: instantiate a component, mutate it, and assert against #prints or #invalidated?. It does not run an event loop, so it is not suitable for system-testing whole apps — for that, drive the real script through a PTY (see spec/examples/).

It also turns the stale-rect diagnostic on (StrictLayout): a rect read before the layout has settled raises here, where in an app it would quietly answer the previous pass's rectangle. Tuile.strict_layout = false opts a suite out; without_strict_layout opts one read out.

Call Screen.fake to initialize the fake screen easily. Typical usage:

before { Screen.fake }
after  { Screen.close }

it "paints its content" do
label = Component::Label.new.tap { |l| l.text = "hi" }
Screen.instance.content = Component::Window.new("Greeting").tap { |w| w.content = label }
Screen.instance.repaint
assert_includes Screen.instance.prints.join, "hi"
end

Constant Summary collapse

EDITING_KEYS =

Returns:

  • (::Array[String])

Instance Attribute Summary collapse

Attributes inherited from Screen

#background_color, #buffer, #canvas, #color_depth, #color_scheme, #event_queue, #focused, #locale, #pane, #size, #theme, #theme_def

Instance Method Summary collapse

Methods inherited from Screen

#add_popup, #beep, #canvas_for, #check_locked, #clip_for, #clipped?, close, #close, #content, #content=, #cursor_position, #cursor_sequence, #cycle_focus, #event_loop, fake, #fire_focus_hooks, #flush_layout, #focus_next, #focus_previous, #follow_up_focus, #grabbed, #handle_background_color, #handle_color_scheme, #handle_key?, #handle_mouse, #handle_paste, #has_popup?, #hovered, instance, instance?, #invalidate, #invalidate_layout, listener, #needs_full_repaint, #on_error, #on_focus_changed, #popups, #register_global_shortcut, #remove_popup, #repaint, #resize, #restyle, #run_event_loop, #settle, #size_pane, #state, #unregister_global_shortcut, #warn_if_unseen

Methods included from Listeners::Declare

#listener

Constructor Details

#initialize(width: 160, height: 50) ⇒ FakeScreen

The terminal starts at the size given, as if it had always been that big: nothing is dispatched and nothing repaints. To exercise a change of size, the SIGWINCH path, call #resize_terminal instead.

@param width — the terminal's columns.

@param height — the terminal's rows.

Parameters:

  • width: (Integer) (defaults to: 160)
  • height: (Integer) (defaults to: 50)


36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/tuile/fake_screen.rb', line 36

def initialize(width: 160, height: 50)
  super()
  # `Tuile.strict_layout` defaults to `:raise` under this screen; this puts
  # the check it answers through into `Component#rect`.
  StrictLayout.install
  @event_queue = FakeEventQueue.new
  @size = Size.new(width, height)
  # super sized both to the test runner's TTY.
  @buffer.resize(@size)
  size_pane
  @prints = []
end

Instance Attribute Details

#prints ⇒ ::Array[String] (readonly)

@return — whatever #print / #emit produced so far. Component painting lands in Screen#buffer, not here — assert on Buffer#row_text / Buffer#row_ansi / Buffer#cell for content, and on prints for cursor and housekeeping escapes.

Returns:

  • (::Array[String])


53
54
55
# File 'lib/tuile/fake_screen.rb', line 53

def prints
  @prints
end

Instance Method Details

#background_color=(color) ⇒ void

This method returns an undefined value.

Plays the terminal answering the OSC 11 re-probe, so a spec can exercise app code that derives colors from Screen#background_color:

Screen.instance.background_color = Color.rgb(30, 30, 46)

Takes the same path a real reply does — a changed color fires Component#handle_theme_changed across the tree and invalidates it. There is no such writer on Screen: the value is a report from the terminal, not a setting.

@param color

Parameters:



110
111
112
# File 'lib/tuile/fake_screen.rb', line 110

def background_color=(color)
  handle_background_color(color)
end

#clear ⇒ void

This method returns an undefined value.



56
57
58
# File 'lib/tuile/fake_screen.rb', line 56

def clear
  @prints.clear
end

#click(x, y, button: :left) ⇒ void

This method returns an undefined value.

Plays a whole click at a screen cell — the press, then the release that ends its grab:

screen.click(save_button.absolute_rect.left, save_button.absolute_rect.top)

Routed exactly as the terminal's own report would be (Mouse::Router), so it focuses, dismisses popups and bubbles.

@param x — 0-based column.

@param y — 0-based row.

@param button — :left, :middle or :right.

Parameters:

  • x (Integer)
  • y (Integer)
  • button: (Symbol) (defaults to: :left)


125
126
127
128
# File 'lib/tuile/fake_screen.rb', line 125

def click(x, y, button: :left)
  press(x, y, button: button)
  release(x, y)
end

#coerce_point(point) ⇒ Point

@param point

Parameters:

  • point (Point, [Integer, Integer])

Returns:



205
206
207
208
209
210
211
212
213
214
# File 'lib/tuile/fake_screen.rb', line 205

def coerce_point(point)
  case point
  when Point then point
  when Array
    raise ArgumentError, "expected [x, y], got #{point.inspect}" unless point.size == 2

    Point.new(point[0], point[1])
  else raise ArgumentError, "expected a Point or [x, y], got #{point.inspect}"
  end
end

#detect_background ⇒ TerminalBackground::Result

No terminal probing in tests: skip TerminalBackground.detect (which would write an OSC 11 query to the test runner's TTY and steal its input) and pin the deterministic default. The color is nil — the case every app must handle anyway — until a spec assigns one through #background_color=.



222
# File 'lib/tuile/fake_screen.rb', line 222

def detect_background = TerminalBackground::Result.new(scheme: :dark, color: nil)

#detect_color_depth ⇒ Symbol

Pins the depth rather than reading the test runner's environment, so a spec asserting flushed bytes gets the same answer on a truecolor terminal, under TERM=dumb in CI, and inside tmux. A spec exercising degradation builds its own Buffer with the depth it wants.

Returns:

  • (Symbol)


229
# File 'lib/tuile/fake_screen.rb', line 229

def detect_color_depth = :truecolor

#detect_locale ⇒ Locale

Pins the conventions instead of probing, so a spec asserting a painted date gets the same answer under LC_TIME=en_DK as under LANG=C — and so no example pays for a locale(1) subprocess. A spec exercising detection assigns Screen#locale= or calls Locale.from_keywords with canned answers.

Returns:



237
# File 'lib/tuile/fake_screen.rb', line 237

def detect_locale = Locale::ISO

#dispatch(event) ⇒ Object

Settles the layout on the way in as well as out. A spec mutates between gestures, where the loop would have settled at the end of the previous dispatch and there is none — and routing a press reads rects, so without this a click hit-tests what the last mutation left half-finished.

@param event — see Screen#dispatch.

@return — whatever the handler returned.

Parameters:

  • event (Object)

Returns:

  • (Object)


198
199
200
201
# File 'lib/tuile/fake_screen.rb', line 198

def dispatch(event)
  flush_layout
  super
end

#drag(*points, button: :left) ⇒ void

This method returns an undefined value.

Plays a whole drag: the press at the first point, one move per point after it, and the release at the last.

screen.drag([2, 1], [3, 2], [4, 3])         # three reports, a diagonal
screen.drag(canvas.rect.top_left, [9, 9])   # the coarsest drag there is

Reports only the points given, as the wire does: at ~84 reports a second (R_mouse_reporting) a quick drag genuinely skips cells, so interpolating them would let a spec assert a continuity no terminal delivers. A one-point drag is a #click — use that.

@param points — two or more positions, as Points or [x, y] pairs.

@param button — :left, :middle or :right.

Parameters:

  • points (::Array[(Point | [Integer, Integer])])
  • button: (Symbol) (defaults to: :left)


181
182
183
184
185
186
187
188
# File 'lib/tuile/fake_screen.rb', line 181

def drag(*points, button: :left)
  raise ArgumentError, "a drag needs at least two points, got #{points.size}" if points.size < 2

  path = points.map { coerce_point(_1) }
  press(path.first.x, path.first.y, button: button)
  path.drop(1).each { move(_1.x, _1.y, button: button) }
  release(path.last.x, path.last.y)
end

#emit(str) ⇒ void

This method returns an undefined value.

Captures the assembled repaint frame instead of writing to the test runner's TTY. Lands in #prints so cursor/sync escapes can be asserted; painted content is read from Screen#buffer.

@param str

Parameters:

  • str (String)


72
73
74
# File 'lib/tuile/fake_screen.rb', line 72

def emit(str)
  @prints << str
end

#invalidated?(component) ⇒ Boolean

@param component — the component to check.

Parameters:

Returns:

  • (Boolean)


92
# File 'lib/tuile/fake_screen.rb', line 92

def invalidated?(component) = @invalidated.include?(component)

#invalidated_clear ⇒ void

This method returns an undefined value.



95
96
97
# File 'lib/tuile/fake_screen.rb', line 95

def invalidated_clear
  @invalidated.clear
end

#move(x, y, button: nil) ⇒ void

This method returns an undefined value.

Moves the pointer, firing the enter/exit hooks the new position implies — or, while a press is grabbed, one Component#handle_mouse_drag.

@param x — 0-based column.

@param y — 0-based row.

@param button — the button held while moving, if any.

Parameters:

  • x (Integer)
  • y (Integer)
  • button: (Symbol, nil) (defaults to: nil)


164
# File 'lib/tuile/fake_screen.rb', line 164

def move(x, y, button: nil) = dispatch(Mouse::MoveEvent.new(button, x, y))

#paste(text) ⇒ Boolean

Pastes text into the focused component, as a real terminal would with bracketed paste on:

area.focus
Screen.instance.paste("one\r\ntwo")
area.text   # => "one\ntwo" — one mutation, no ENTER anywhere

Goes through Keys.normalize_paste first, so a spec can hand it the CR-flavored line endings terminals actually deliver and still assert against \n.

@param text

@return — true if some component consumed it.

Parameters:

  • text (String)

Returns:

  • (Boolean)


88
# File 'lib/tuile/fake_screen.rb', line 88

def paste(text) = dispatch(EventQueue::PasteEvent.new(Keys.normalize_paste(text)))

#press(x, y, button: :left) ⇒ void

This method returns an undefined value.

Half a #click, for a spec about the grab — what is claimed, what the drag does, what the release lands on.

@param x — 0-based column.

@param y — 0-based row.

@param button — :left, :middle or :right.

Parameters:

  • x (Integer)
  • y (Integer)
  • button: (Symbol) (defaults to: :left)


143
# File 'lib/tuile/fake_screen.rb', line 143

def press(x, y, button: :left) = dispatch(Mouse::DownEvent.new(button, x, y))

This method returns an undefined value.

Doesn't print anything: collects all strings in #prints.

@param args

Parameters:

  • args (String)


63
64
65
# File 'lib/tuile/fake_screen.rb', line 63

def print(*args)
  @prints += args
end

#release(x, y) ⇒ void

This method returns an undefined value.

The other half of #press.

@param x — 0-based column.

@param y — 0-based row.

Parameters:

  • x (Integer)
  • y (Integer)


149
# File 'lib/tuile/fake_screen.rb', line 149

def release(x, y) = dispatch(Mouse::UpEvent.new(x, y))

#resize_terminal(width, height) ⇒ void

This method returns an undefined value.

The terminal reporting a new size, routed as its own report would be: the pane takes the whole screen, and every popup is placed again.

@param width

@param height

Parameters:

  • width (Integer)
  • height (Integer)


135
# File 'lib/tuile/fake_screen.rb', line 135

def resize_terminal(width, height) = dispatch(EventQueue::TTYSizeEvent.new(width, height))

#scroll(direction, x, y) ⇒ void

This method returns an undefined value.

One wheel notch over a cell.

@param direction — :up, :down, :left or :right.

@param x — 0-based column.

@param y — 0-based row.

Parameters:

  • direction (Symbol)
  • x (Integer)
  • y (Integer)


156
# File 'lib/tuile/fake_screen.rb', line 156

def scroll(direction, x, y) = dispatch(Mouse::ScrollEvent.new(direction, x, y))