howdoc 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 168c62956710423757404d82987844f3b33d64ebdc112f9c5c0354cd336c0944
4
- data.tar.gz: 842bebc8325f407a398af4f87e050819325cdafea0d26ab568a694c91f3c765f
3
+ metadata.gz: 9201c07602657e340541387ddb7704ca43fcde6183ce52fd1273f036dd87513b
4
+ data.tar.gz: 2f9bc01470635e9d626141909f12196cb950e41e80c11077aa78943216291259
5
5
  SHA512:
6
- metadata.gz: 139dfdadfe51e236abfb19bff5cb95bd28d6cb8ddd7e817b1ca9cb93362cd5cb3b9b83313544c3b9d35423f58a1385d2db807794dd2723f1e0fd4287732f34ac
7
- data.tar.gz: 57d7cc5bafa03b0b103204e3a13a04015d5676445050a621cbca7b8be6e2290b01fd39bd4dd8db30377030277b66c01f57780b8462358a9a23a3b05be179159d
6
+ metadata.gz: c970647ba28984988f284e173b3a675123cee78b35a66a1c0ff8c5d7ed4d6ca856e41dbf3b7e8eade8a4b852121a636d86869d1f002007f5df3eba6b5c9ab10a
7
+ data.tar.gz: ea877687bd2a0ef1c550769f96d5cfea425ccda4f74ee1d4d71dde7575f5f473c13e97a13998d71f9958d01aa6fbeed69ca3daeb056ad0448e32f5f2ab42d3a9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ Screenshots can now show what the step is about, the way a guide made by hand
6
+ does: an arrow on the button being pressed, a highlight around the words being
7
+ looked for.
8
+
9
+ - The marker is drawn in the browser between sizing the window and taking the
10
+ picture, and removed again straight after -- including when the picture could
11
+ not be taken, so nothing is left pointing at what has since moved.
12
+ - An arrow's tip goes on the middle of a button, and below anything small
13
+ enough that the arrow would cover it.
14
+ - A highlight measures the words themselves, through a range around the match,
15
+ rather than the paragraph that happens to hold them.
16
+ - `config.illustrate_actions` illustrates every action rather than only the
17
+ moments a reader arrives somewhere. That is what makes the pointer worth
18
+ having: a picture per click, each one marked.
19
+ - `config.marker`, `config.marker_colour`, `config.marker_dim` and
20
+ `config.marker_size` decide whether and how it is drawn.
21
+
3
22
  ## 0.3.1
4
23
 
5
24
  - The menu pass left a blank line behind every time it ran. A suite that builds
data/README.md CHANGED
@@ -114,6 +114,31 @@ By default `visit` and `assert_text` are illustrated and the action verbs are
114
114
  not, on the grounds that a picture is worth showing when the reader arrives
115
115
  somewhere, not every time they press a button.
116
116
 
117
+ ## Showing where to look
118
+
119
+ A screenshot of a busy page does not say which of forty things on it the step is
120
+ about, so howdoc draws the mark a person would draw by hand:
121
+
122
+ * an arrow with its tip on the button about to be pressed, on the field about to
123
+ be filled, on the menu about to be chosen from
124
+ * a highlight around the words an assertion was looking for
125
+
126
+ It is drawn in the browser between sizing the window and taking the picture, and
127
+ taken off again straight after, so the page the test goes on driving is the page
128
+ it was. There is no cursor in a headless screenshot to photograph -- the arrow is
129
+ drawn, and its tip moves below anything too small for it to sit on without
130
+ covering.
131
+
132
+ By default only the moments a reader arrives somewhere are illustrated. A guide
133
+ that shows every click is one line of configuration:
134
+
135
+ ```ruby
136
+ config.illustrate_actions = true # a picture per action, each one marked
137
+ config.marker_colour = '#e11d48' # the arrow and the highlight
138
+ config.marker_dim = true # dim the rest of the page around the mark
139
+ config.marker = false # or draw nothing at all
140
+ ```
141
+
117
142
  ## Your own helpers
118
143
 
119
144
  Real suites have helpers Capybara has no verb for -- a custom editor, a menu
@@ -30,7 +30,10 @@ module Howdoc
30
30
  def fill_in(locator = nil, with:, currently_with: nil, fill_options: {}, **options)
31
31
  doc = Howdoc.extract_options!(options)
32
32
 
33
- narrate(fill_in_action(locator), doc, field: howdoc_field_label(locator), value: with)
33
+ narrate(
34
+ fill_in_action(locator), doc,
35
+ target: [:fillable_field, locator], field: howdoc_field_label(locator), value: with
36
+ )
34
37
 
35
38
  super(locator, with:, currently_with:, fill_options:, **options)
36
39
  end
@@ -38,7 +41,7 @@ module Howdoc
38
41
  def select(value = nil, from: nil, **options)
39
42
  doc = Howdoc.extract_options!(options)
40
43
 
41
- narrate(:select, doc, value:, field: howdoc_field_label(from))
44
+ narrate(:select, doc, target: [:select, from], value:, field: howdoc_field_label(from))
42
45
 
43
46
  super(value, from:, **options)
44
47
  end
@@ -46,7 +49,7 @@ module Howdoc
46
49
  def check(locator = nil, **options)
47
50
  doc = Howdoc.extract_options!(options)
48
51
 
49
- narrate(:check, doc, field: howdoc_field_label(locator))
52
+ narrate(:check, doc, target: [:checkbox, locator], field: howdoc_field_label(locator))
50
53
 
51
54
  super(locator, **options)
52
55
  end
@@ -54,7 +57,7 @@ module Howdoc
54
57
  def uncheck(locator = nil, **options)
55
58
  doc = Howdoc.extract_options!(options)
56
59
 
57
- narrate(:uncheck, doc, field: howdoc_field_label(locator))
60
+ narrate(:uncheck, doc, target: [:checkbox, locator], field: howdoc_field_label(locator))
58
61
 
59
62
  super(locator, **options)
60
63
  end
@@ -62,7 +65,7 @@ module Howdoc
62
65
  def choose(locator = nil, **options)
63
66
  doc = Howdoc.extract_options!(options)
64
67
 
65
- narrate(:choose, doc, field: howdoc_field_label(locator))
68
+ narrate(:choose, doc, target: [:radio_button, locator], field: howdoc_field_label(locator))
66
69
 
67
70
  super(locator, **options)
68
71
  end
@@ -70,7 +73,7 @@ module Howdoc
70
73
  def click_button(locator = nil, **options)
71
74
  doc = Howdoc.extract_options!(options)
72
75
 
73
- narrate(:click_button, doc, locator:)
76
+ narrate(:click_button, doc, target: [:button, locator], locator:)
74
77
 
75
78
  super(locator, **options)
76
79
  end
@@ -78,7 +81,7 @@ module Howdoc
78
81
  def click_link(locator = nil, **options)
79
82
  doc = Howdoc.extract_options!(options)
80
83
 
81
- narrate(:click_link, doc, locator:)
84
+ narrate(:click_link, doc, target: [:link, locator], locator:)
82
85
 
83
86
  super(locator, **options)
84
87
  end
@@ -86,7 +89,7 @@ module Howdoc
86
89
  def click_on(locator = nil, **options)
87
90
  doc = Howdoc.extract_options!(options)
88
91
 
89
- narrate(:click_on, doc, locator:)
92
+ narrate(:click_on, doc, target: [:link_or_button, locator], locator:)
90
93
 
91
94
  super(locator, **options)
92
95
  end
@@ -95,7 +98,7 @@ module Howdoc
95
98
  def attach_file(locator = nil, paths, make_visible: nil, **options)
96
99
  doc = Howdoc.extract_options!(options)
97
100
 
98
- narrate(:attach_file, doc, field: howdoc_field_label(locator))
101
+ narrate(:attach_file, doc, target: [:file_field, locator], field: howdoc_field_label(locator))
99
102
 
100
103
  super(locator, paths, make_visible:, **options)
101
104
  end
@@ -107,7 +110,10 @@ module Howdoc
107
110
  doc = Howdoc.extract_options!(options)
108
111
  result = super(locator, **options)
109
112
 
110
- narrate(:assert_text, doc, capture: true, arrival: true, text: locator)
113
+ narrate(
114
+ :assert_text, doc,
115
+ capture: true, arrival: true, text: locator, target: locator.to_s, marker_mode: :highlight
116
+ )
111
117
  result
112
118
  end
113
119
 
@@ -138,19 +144,48 @@ module Howdoc
138
144
  host.nil? ? visit_uri.to_s : "https://#{host}#{visit_uri}"
139
145
  end
140
146
 
141
- def narrate(action, doc_options, capture: false, arrival: false, **payload)
147
+ def narrate(action, doc_options, capture: false, arrival: false, target: nil,
148
+ marker_mode: :pointer, **payload)
142
149
  step = Howdoc.record(action, arrival:, nodoc: doc_options[:nodoc], **payload)
143
150
  return nil if step.nil?
144
151
 
145
- illustrate = doc_options.fetch(:screenshot, capture)
152
+ illustrate = doc_options.fetch(:screenshot, capture || illustrated_action?(target))
146
153
  return step unless illustrate
147
154
 
148
155
  Howdoc.capture(
149
156
  page,
150
157
  full_page: doc_options[:full_page] || false,
151
- no_screenshot: doc_options[:no_screenshot] || false
158
+ no_screenshot: doc_options[:no_screenshot] || false,
159
+ marker: howdoc_marker(target), marker_mode:
152
160
  )
153
161
  step
154
162
  end
163
+
164
+ # An action is illustrated only when the application asked for every click
165
+ # to be shown; a picture after each button is a lot of pictures, and worth
166
+ # it only when they are marked.
167
+ def illustrated_action?(target)
168
+ !target.nil? && Howdoc.config.illustrate_actions?
169
+ end
170
+
171
+ # What the marker should point at. A string is the words an assertion was
172
+ # looking for, which the browser finds for itself; anything else names a
173
+ # Capybara selector and is resolved here, in whatever scope the test is in,
174
+ # exactly as the action about to happen would resolve it.
175
+ #
176
+ # Never waits: the action itself is entitled to wait, but a picture is not
177
+ # worth holding a suite up for, and a marker nobody could place is simply
178
+ # not drawn.
179
+ def howdoc_marker(target)
180
+ return nil if target.nil? || !Howdoc.config.marker?
181
+ return target if target.is_a?(String)
182
+
183
+ kind, locator = target
184
+ return nil if locator.nil?
185
+
186
+ page.find(kind, locator, match: :first, wait: 0)
187
+ rescue StandardError
188
+ nil
189
+ end
155
190
  end
156
191
  end
@@ -35,6 +35,29 @@ module Howdoc
35
35
  # page, so that guides do not mix wildly different image proportions.
36
36
  attr_accessor :screenshot_height
37
37
 
38
+ # Whether a screenshot points at what the step is about: an arrow on the
39
+ # button being pressed, a highlight around the words being looked for. Off
40
+ # leaves the pictures exactly as the browser rendered them.
41
+ attr_accessor :marker
42
+
43
+ # The colour of that arrow and highlight. Red by convention, because a guide
44
+ # is read at a glance and red is the one colour an interface rarely uses for
45
+ # anything else.
46
+ attr_accessor :marker_colour
47
+
48
+ # Dims the rest of the page around what is marked. Emphatic, and worth it
49
+ # for a crowded screen; off by default because it changes every picture.
50
+ attr_accessor :marker_dim
51
+
52
+ # How tall the drawn pointer is, in CSS pixels.
53
+ attr_accessor :marker_size
54
+
55
+ # Whether an action -- a click, a choice, something typed -- is illustrated
56
+ # as well as narrated. Off by default: a picture belongs where the reader
57
+ # arrives somewhere new, not after every button. Turning it on is what makes
58
+ # the pointer worth having, so a guide shows every click.
59
+ attr_accessor :illustrate_actions
60
+
38
61
  # Files in the output directory that the clean task must leave alone,
39
62
  # typically hand-maintained landing pages.
40
63
  attr_accessor :preserved_files
@@ -53,19 +76,28 @@ module Howdoc
53
76
  attr_reader :locale_paths
54
77
 
55
78
  def initialize
79
+ reset_defaults
80
+ @template_paths = []
81
+ @locale_paths = []
82
+ @field_label = nil
83
+ @sort_key = nil
84
+ @group_label = nil
85
+ end
86
+
87
+ def reset_defaults
56
88
  @root = 'doc/howdoc'
57
89
  @enabled = false
58
90
  @host = nil
59
91
  @formats = %i[html]
60
92
  @locales = nil
61
93
  @screenshot_height = 940
94
+ @marker = true
95
+ @marker_colour = '#e11d48'
96
+ @marker_dim = false
97
+ @marker_size = 52
98
+ @illustrate_actions = false
62
99
  @preserved_files = %w[index.html .keep]
63
100
  @install_assets = true
64
- @template_paths = []
65
- @locale_paths = []
66
- @field_label = nil
67
- @sort_key = nil
68
- @group_label = nil
69
101
  end
70
102
 
71
103
  def register_template_path(path)
@@ -105,5 +137,13 @@ module Howdoc
105
137
  def enabled?
106
138
  !!@enabled
107
139
  end
140
+
141
+ def marker?
142
+ !!@marker
143
+ end
144
+
145
+ def illustrate_actions?
146
+ !!@illustrate_actions
147
+ end
108
148
  end
109
149
  end
@@ -0,0 +1,113 @@
1
+ // Drawn over the page for as long as it takes to photograph it: an arrow on
2
+ // what is about to be clicked, a highlight around the words that were looked
3
+ // for. Called with no target, it takes off whatever it drew last time.
4
+ var MARKER = 'howdoc-marker';
5
+ var POINTER =
6
+ // The arrow, in a 24x32 box with its tip at 0,0 -- the shape every operating
7
+ // system has drawn since 1981, so a reader knows it without being told.
8
+ 'M0 0 L0 22.6 L5.6 17.4 L9.4 25.9 L13.6 23.9 L9.8 15.7 L17.6 15.4 Z';
9
+
10
+ var target = arguments[0];
11
+ var options = arguments[1] || {};
12
+
13
+ var previous = document.getElementById(MARKER);
14
+ if (previous) { previous.parentNode.removeChild(previous); }
15
+ if (!target) { return null; }
16
+
17
+ var element = target;
18
+ var rect = null;
19
+
20
+ if (typeof target === 'string') {
21
+ // The words themselves, not the paragraph holding them: a range around
22
+ // the match measures what a reader would underline with a finger, where
23
+ // the element around it is as wide as the page.
24
+ var wanted = target.replace(/\\s+/g, ' ').trim();
25
+ var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
26
+ var node = null;
27
+ element = null;
28
+
29
+ while ((node = walker.nextNode())) {
30
+ var at = node.data.indexOf(wanted);
31
+ if (at === -1 || !node.parentElement || !node.parentElement.getClientRects().length) {
32
+ continue;
33
+ }
34
+
35
+ var range = document.createRange();
36
+ range.setStart(node, at);
37
+ range.setEnd(node, at + wanted.length);
38
+ rect = range.getBoundingClientRect();
39
+ element = node.parentElement;
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (!element || !element.getClientRects().length) { return null; }
45
+ if (!rect) { rect = element.getBoundingClientRect(); }
46
+
47
+ // A field dressed by a widget -- select2 and its like -- leaves the real
48
+ // <select> hidden and all but sizeless in a corner of what the reader actually
49
+ // sees. Point at the thing on the screen: the nearest ancestor that is really
50
+ // laid out.
51
+ if (rect.width < 8 || rect.height < 8) {
52
+ var holder = element.parentElement;
53
+
54
+ while (holder && holder !== document.body) {
55
+ var box = holder.getBoundingClientRect();
56
+
57
+ if (box.width >= 8 && box.height >= 8) { element = holder; rect = box; break; }
58
+
59
+ holder = holder.parentElement;
60
+ }
61
+ }
62
+ var left = rect.left + window.scrollX;
63
+ var top = rect.top + window.scrollY;
64
+ var colour = options.colour || '#e11d48';
65
+ var padding = 6;
66
+
67
+ var layer = document.createElement('div');
68
+ layer.id = MARKER;
69
+ layer.style.cssText = 'position:absolute;left:0;top:0;width:0;height:0;' +
70
+ 'z-index:2147483647;pointer-events:none;';
71
+
72
+ var ring = document.createElement('div');
73
+ ring.style.cssText = 'position:absolute;box-sizing:border-box;' +
74
+ 'left:' + (left - padding) + 'px;top:' + (top - padding) + 'px;' +
75
+ 'width:' + (rect.width + padding * 2) + 'px;' +
76
+ 'height:' + (rect.height + padding * 2) + 'px;' +
77
+ 'border:3px solid ' + colour + ';border-radius:8px;' +
78
+ (options.mode === 'highlight' ? 'background:' + colour + '1f;' : '') +
79
+ (options.dim ? 'box-shadow:0 0 0 9999px rgba(15,23,42,0.45);' : '');
80
+ layer.appendChild(ring);
81
+
82
+ if (options.mode !== 'highlight') {
83
+ var size = options.size || 52;
84
+
85
+ // The tip goes on the middle of a button, where a hand would put it, and a
86
+ // little below the middle on a field or a select: there the value is read
87
+ // along the top of the box, and an arrow through the middle of it sits in
88
+ // the way of the very thing the reader is meant to see.
89
+ //
90
+ // On something small -- a link in a table, a word in a row -- the arrow would
91
+ // cover what it points at whatever we do, so it hangs below instead.
92
+ var small = rect.width < 120 || rect.height < 32;
93
+ var tipLeft = left + rect.width / 2;
94
+ var tipTop = small ? top + rect.height - 2 : top + rect.height * 0.62;
95
+ var arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
96
+ arrow.setAttribute('viewBox', '0 0 24 32');
97
+ arrow.setAttribute('width', size);
98
+ arrow.setAttribute('height', size * 32 / 24);
99
+ arrow.style.cssText = 'position:absolute;left:' + tipLeft + 'px;top:' + tipTop + 'px;' +
100
+ 'filter:drop-shadow(0 2px 3px rgba(15,23,42,0.45));';
101
+
102
+ var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
103
+ path.setAttribute('d', POINTER);
104
+ path.setAttribute('fill', '#ffffff');
105
+ path.setAttribute('stroke', colour);
106
+ path.setAttribute('stroke-width', '2.2');
107
+ path.setAttribute('stroke-linejoin', 'round');
108
+ arrow.appendChild(path);
109
+ layer.appendChild(arrow);
110
+ }
111
+
112
+ document.body.appendChild(layer);
113
+ return true;
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howdoc
4
+ # Shows the reader where to look.
5
+ #
6
+ # A screenshot of a busy page does not say which of forty things on it the
7
+ # step is about. A guide made by hand solves that with a red circle drawn over
8
+ # the button; this draws the same thing, in the browser, in the moment between
9
+ # the window being sized and the picture being taken -- and takes it off again
10
+ # afterwards, so the page the test goes on driving is the page it was.
11
+ #
12
+ # There is no cursor in a headless screenshot to photograph, so the pointer is
13
+ # drawn: an arrow with its tip on whatever is about to be clicked. An assertion
14
+ # gets no arrow, because nobody is pointing at anything -- the words the reader
15
+ # should see are highlighted instead.
16
+ #
17
+ # The drawing itself is JavaScript, and lives in JavaScript: marker.js beside
18
+ # this file. Asked to draw nothing, it takes off what it drew last time, which
19
+ # is how the marker is removed.
20
+ module Marker
21
+ SCRIPT = File.read(File.expand_path('marker.js', __dir__)).freeze
22
+
23
+ module_function
24
+
25
+ # Draws the marker, runs the block, and takes it off again -- including when
26
+ # the block raised, because a marker left behind would appear in the next
27
+ # picture, pointing at something that is no longer there.
28
+ def around(page, target, mode:)
29
+ return yield if target.nil? || !Howdoc.config.marker?
30
+
31
+ drawn = draw(page, target, mode:)
32
+
33
+ begin
34
+ yield
35
+ ensure
36
+ draw(page, nil, mode:) if drawn
37
+ end
38
+ end
39
+
40
+ def draw(page, target, mode:)
41
+ page.execute_script(SCRIPT, target, options(mode))
42
+ true
43
+ rescue StandardError
44
+ # A marker is a courtesy. A page that will not take one -- a frame gone, an
45
+ # element detached between finding it and drawing on it -- still has a
46
+ # picture worth taking.
47
+ false
48
+ end
49
+
50
+ def options(mode)
51
+ {
52
+ 'mode' => mode.to_s,
53
+ 'colour' => Howdoc.config.marker_colour,
54
+ 'dim' => Howdoc.config.marker_dim,
55
+ 'size' => Howdoc.config.marker_size
56
+ }
57
+ end
58
+ end
59
+ end
@@ -54,7 +54,7 @@ module Howdoc
54
54
  # Illustrates the step recorded most recently. Separate from +record+
55
55
  # because a screenshot has to be taken while the browser is still on the
56
56
  # page, whereas the sentence can be composed at any time.
57
- def capture(page, full_page: false, no_screenshot: false)
57
+ def capture(page, full_page: false, no_screenshot: false, marker: nil, marker_mode: :pointer)
58
58
  return nil if no_screenshot || !recording?
59
59
 
60
60
  step = current.last_step || current.new_step
@@ -64,7 +64,7 @@ module Howdoc
64
64
  page,
65
65
  path: File.join(current.image_dir, filename),
66
66
  height: Howdoc.config.screenshot_height,
67
- full_page:
67
+ full_page:, marker:, marker_mode:
68
68
  )
69
69
 
70
70
  step.screenshot = filename
@@ -7,7 +7,10 @@ module Howdoc
7
7
  module Screenshot
8
8
  module_function
9
9
 
10
- def capture(page, path:, height:, full_page: false)
10
+ # The marker is drawn after the window has been sized and not before: a
11
+ # resize lays the page out again, and an arrow placed by the old layout
12
+ # would point at where the button used to be.
13
+ def capture(page, path:, height:, full_page: false, marker: nil, marker_mode: :pointer)
11
14
  FileUtils.mkdir_p(File.dirname(path))
12
15
 
13
16
  width, current_height = page.current_window.size
@@ -15,7 +18,10 @@ module Howdoc
15
18
 
16
19
  page.current_window.resize_to(width, target) unless target == current_height
17
20
 
18
- page.save_screenshot(path, full: true)
21
+ Marker.around(page, marker, mode: marker_mode) do
22
+ page.save_screenshot(path, full: true)
23
+ end
24
+
19
25
  path
20
26
  end
21
27
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- VERSION = '0.3.1'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/howdoc.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'howdoc/narrator'
9
9
  require_relative 'howdoc/step'
10
10
  require_relative 'howdoc/edition'
11
11
  require_relative 'howdoc/document'
12
+ require_relative 'howdoc/marker'
12
13
  require_relative 'howdoc/screenshot'
13
14
  require_relative 'howdoc/recorder'
14
15
  require_relative 'howdoc/templates'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howdoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Priit Tark
@@ -59,6 +59,8 @@ files:
59
59
  - lib/howdoc/configuration.rb
60
60
  - lib/howdoc/document.rb
61
61
  - lib/howdoc/edition.rb
62
+ - lib/howdoc/marker.js
63
+ - lib/howdoc/marker.rb
62
64
  - lib/howdoc/minitest.rb
63
65
  - lib/howdoc/narrator.rb
64
66
  - lib/howdoc/navigation.rb