howdoc 0.3.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +30 -0
- data/config/locales/howdoc.en.yml +2 -0
- data/config/locales/howdoc.et.yml +2 -0
- data/lib/howdoc/capybara_actions.rb +48 -13
- data/lib/howdoc/configuration.rb +45 -5
- data/lib/howdoc/marker.js +113 -0
- data/lib/howdoc/marker.rb +59 -0
- data/lib/howdoc/recorder.rb +2 -2
- data/lib/howdoc/screenshot.rb +8 -2
- data/lib/howdoc/version.rb +1 -1
- data/lib/howdoc.rb +1 -0
- data/templates/default/assets/howdoc.css +101 -6
- data/templates/default/shared/html/nav.haml +18 -11
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87dfef0ef3ca14057c666eaa64c003057f7b7f8ba17cb1f992375c4add7358e6
|
|
4
|
+
data.tar.gz: 5a3f3ecfccd3e57262e00b434ba69d79874251d25e74b527e8a6d03754e5c0ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80ffc63f82dd61163966cd9afa236ff758b29ad02bf1dcf54486d41db1fcdee943bcbd337d0966a9a6c69aae624355d43b732164ac31425483dabc9c395812d0
|
|
7
|
+
data.tar.gz: 756e73c34a33a1a4b5cdb0b41f4cf2143414c3b97bc30096e62511d38961ff8cc07b44ba9e35c2956b0cb0e942bd9d669bb8574ec2d578a6dba368c8dce5135d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
The menu now fits a phone. Twenty guides listed above the page filled the
|
|
6
|
+
screen before the guide a reader had come for began, so a narrow screen folds
|
|
7
|
+
the list away behind a line they can tap, and a wide one, which has room for it
|
|
8
|
+
beside the page, shows it exactly as before.
|
|
9
|
+
|
|
10
|
+
- The menu on a narrow screen is a slim bar carrying the name of the
|
|
11
|
+
documentation set, the languages and one line to open the guides
|
|
12
|
+
(`howdoc.nav.guides`). It follows the reader down the page while the list is
|
|
13
|
+
folded, and steps out of the way while it is open.
|
|
14
|
+
- The index is the one page whose menu is the page, so there the list is open
|
|
15
|
+
from the start and follows the heading that says what the guides are for
|
|
16
|
+
instead of pushing it under twenty links.
|
|
17
|
+
- Only `shared/html/nav.haml` and the stylesheet changed, so an application
|
|
18
|
+
with a menu template of its own keeps what it has.
|
|
19
|
+
|
|
20
|
+
## 0.4.0
|
|
21
|
+
|
|
22
|
+
Screenshots can now show what the step is about, the way a guide made by hand
|
|
23
|
+
does: an arrow on the button being pressed, a highlight around the words being
|
|
24
|
+
looked for.
|
|
25
|
+
|
|
26
|
+
- The marker is drawn in the browser between sizing the window and taking the
|
|
27
|
+
picture, and removed again straight after -- including when the picture could
|
|
28
|
+
not be taken, so nothing is left pointing at what has since moved.
|
|
29
|
+
- An arrow's tip goes on the middle of a button, and below anything small
|
|
30
|
+
enough that the arrow would cover it.
|
|
31
|
+
- A highlight measures the words themselves, through a range around the match,
|
|
32
|
+
rather than the paragraph that happens to hold them.
|
|
33
|
+
- `config.illustrate_actions` illustrates every action rather than only the
|
|
34
|
+
moments a reader arrives somewhere. That is what makes the pointer worth
|
|
35
|
+
having: a picture per click, each one marked.
|
|
36
|
+
- `config.marker`, `config.marker_colour`, `config.marker_dim` and
|
|
37
|
+
`config.marker_size` decide whether and how it is drawn.
|
|
38
|
+
|
|
3
39
|
## 0.3.1
|
|
4
40
|
|
|
5
41
|
- 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
|
|
@@ -238,6 +263,11 @@ an empty placeholder and the index pass -- which has just read every page back
|
|
|
238
263
|
|
|
239
264
|
Both are optional. A template that carries neither is left exactly as it is.
|
|
240
265
|
|
|
266
|
+
Above the page rather than beside it -- a phone, a narrow window -- the list of
|
|
267
|
+
guides folds away behind a line the reader taps, so the guide begins where the
|
|
268
|
+
screen begins. The stylesheet unfolds it again where there is room beside the
|
|
269
|
+
page, and the index, whose menu is the page, is open from the start.
|
|
270
|
+
|
|
241
271
|
The menu also links the same guide in every other language the application
|
|
242
272
|
publishes. Guides are matched by their identifier rather than their file name,
|
|
243
273
|
because a translated heading gives a translated file name, and each language
|
|
@@ -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(
|
|
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(
|
|
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,
|
|
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
|
data/lib/howdoc/configuration.rb
CHANGED
|
@@ -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
|
data/lib/howdoc/recorder.rb
CHANGED
|
@@ -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
|
data/lib/howdoc/screenshot.rb
CHANGED
|
@@ -7,7 +7,10 @@ module Howdoc
|
|
|
7
7
|
module Screenshot
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/howdoc/version.rb
CHANGED
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'
|
|
@@ -31,6 +31,12 @@ body.howdoc {
|
|
|
31
31
|
padding: 3rem 1.25rem 6rem;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* The index ends where its menu begins, so the space below it is the menu's
|
|
35
|
+
to give. */
|
|
36
|
+
.howdoc-index {
|
|
37
|
+
padding-bottom: 1.5rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
.howdoc h1 {
|
|
35
41
|
margin: 0 0 1rem;
|
|
36
42
|
font-size: 1.9rem;
|
|
@@ -68,7 +74,7 @@ body.howdoc {
|
|
|
68
74
|
|
|
69
75
|
.howdoc-toc {
|
|
70
76
|
list-style: none;
|
|
71
|
-
margin:
|
|
77
|
+
margin: 1rem 0 0;
|
|
72
78
|
padding: 0;
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -96,19 +102,84 @@ body.howdoc {
|
|
|
96
102
|
text-decoration: underline;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
/* The menu sits beside the page it belongs to, and above it when there
|
|
100
|
-
room beside. A guide written before the index pass has an empty menu,
|
|
101
|
-
empty menu is no menu at all. */
|
|
105
|
+
/* The menu sits beside the page it belongs to, and folded above it when there
|
|
106
|
+
is no room beside. A guide written before the index pass has an empty menu,
|
|
107
|
+
and an empty menu is no menu at all. */
|
|
102
108
|
.howdoc-nav:empty,
|
|
103
109
|
.howdoc-pager:empty {
|
|
104
110
|
display: none;
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
/* Above the page, the menu is a slim bar the reader keeps within reach
|
|
114
|
+
wherever they have scrolled to, with the guides folded away behind a line
|
|
115
|
+
they can tap. Unfolded, the list is taller than the screen, so the bar stops
|
|
116
|
+
following along and scrolls away with the page like anything else. */
|
|
107
117
|
.howdoc-nav {
|
|
108
|
-
|
|
118
|
+
position: sticky;
|
|
119
|
+
top: 0;
|
|
120
|
+
z-index: 2;
|
|
121
|
+
background: var(--howdoc-bg);
|
|
122
|
+
padding: 0.9rem 1.25rem 1rem;
|
|
109
123
|
border-bottom: 1px solid var(--howdoc-rule);
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
.howdoc-nav:has(.howdoc-nav-guides[open]) {
|
|
127
|
+
position: static;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* The index is the one page whose menu is the page, so its menu is open and
|
|
131
|
+
there is nothing to fold. It comes after the heading that says what the
|
|
132
|
+
guides are for, rather than pushing that heading under twenty links. */
|
|
133
|
+
body.howdoc {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.howdoc-nav-index {
|
|
139
|
+
order: 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Nothing to fold on the index: the list is the page, so the line that would
|
|
143
|
+
fold it away is not offered there. */
|
|
144
|
+
.howdoc-nav-index .howdoc-nav-summary {
|
|
145
|
+
display: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.howdoc-nav-summary {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: space-between;
|
|
152
|
+
gap: 0.75rem;
|
|
153
|
+
margin-top: 0.75rem;
|
|
154
|
+
padding: 0.55rem 0.85rem;
|
|
155
|
+
border: 1px solid var(--howdoc-rule);
|
|
156
|
+
border-radius: 8px;
|
|
157
|
+
color: var(--howdoc-fg);
|
|
158
|
+
font-size: 0.95rem;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
list-style: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.howdoc-nav-summary::-webkit-details-marker {
|
|
165
|
+
display: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* The chevron that says the line can be tapped: down while the list is folded,
|
|
169
|
+
up while it is not. */
|
|
170
|
+
.howdoc-nav-summary::after {
|
|
171
|
+
content: '';
|
|
172
|
+
width: 0.45rem;
|
|
173
|
+
height: 0.45rem;
|
|
174
|
+
border-right: 2px solid var(--howdoc-muted);
|
|
175
|
+
border-bottom: 2px solid var(--howdoc-muted);
|
|
176
|
+
transform: translateY(-0.15rem) rotate(45deg);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.howdoc-nav-guides[open] > .howdoc-nav-summary::after {
|
|
180
|
+
transform: translateY(0.15rem) rotate(-135deg);
|
|
181
|
+
}
|
|
182
|
+
|
|
112
183
|
@media (min-width: 60rem) {
|
|
113
184
|
body.howdoc {
|
|
114
185
|
display: grid;
|
|
@@ -116,15 +187,39 @@ body.howdoc {
|
|
|
116
187
|
align-items: start;
|
|
117
188
|
}
|
|
118
189
|
|
|
119
|
-
.howdoc-nav
|
|
190
|
+
.howdoc-nav,
|
|
191
|
+
.howdoc-nav:has(.howdoc-nav-guides[open]) {
|
|
120
192
|
position: sticky;
|
|
121
193
|
top: 0;
|
|
194
|
+
z-index: auto;
|
|
122
195
|
max-height: 100vh;
|
|
123
196
|
overflow-y: auto;
|
|
124
197
|
padding: 2rem 1.25rem 3rem;
|
|
125
198
|
border-right: 1px solid var(--howdoc-rule);
|
|
126
199
|
border-bottom: 0;
|
|
127
200
|
}
|
|
201
|
+
|
|
202
|
+
.howdoc-nav-index {
|
|
203
|
+
order: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Beside the page there is room for the whole list, so the fold is undone:
|
|
207
|
+
the line to tap goes away and every guide is shown whether the reader ever
|
|
208
|
+
tapped it or not. Two rules for two kinds of browser -- the older kind
|
|
209
|
+
folds content away by not displaying it, the newer kind through
|
|
210
|
+
::details-content. */
|
|
211
|
+
.howdoc-nav-summary {
|
|
212
|
+
display: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.howdoc-nav-guides > *:not(summary) {
|
|
216
|
+
display: block;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.howdoc-nav-guides::details-content {
|
|
220
|
+
content-visibility: visible;
|
|
221
|
+
block-size: auto;
|
|
222
|
+
}
|
|
128
223
|
}
|
|
129
224
|
|
|
130
225
|
.howdoc-nav-head {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
-# no other guide exists yet -- so a guide leaves the empty <nav> below and the
|
|
3
3
|
-# index pass fills it in once every guide is on disk. The index renders it
|
|
4
4
|
-# directly, because by then it knows.
|
|
5
|
-
%nav.howdoc-nav{ data: { howdoc_nav: true } }
|
|
5
|
+
%nav.howdoc-nav{ class: ('howdoc-nav-index' if @current.nil?), data: { howdoc_nav: true } }
|
|
6
6
|
.howdoc-nav-head
|
|
7
7
|
%a.howdoc-nav-home{ href: './index.html' }= Howdoc::Narrator.phrase(:index, :title, locale: @locale)
|
|
8
8
|
- if @languages.size > 1
|
|
@@ -13,13 +13,20 @@
|
|
|
13
13
|
%a{ href: language[:href], lang: language[:code] }= language[:label]
|
|
14
14
|
- else
|
|
15
15
|
%span{ lang: language[:code] }= language[:label]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
-# On a phone a list of twenty guides fills the screen before the guide the
|
|
17
|
+
-# reader came to read has begun, so the list is folded away behind a line
|
|
18
|
+
-# they can tap. A wide screen has room for it beside the page and unfolds it
|
|
19
|
+
-# again in the stylesheet. The index is the one page whose menu is the page,
|
|
20
|
+
-# so there it is open from the start.
|
|
21
|
+
%details.howdoc-nav-guides{ open: @current.nil? }
|
|
22
|
+
%summary.howdoc-nav-summary= Howdoc::Narrator.phrase(:nav, :guides, locale: @locale)
|
|
23
|
+
%ul.howdoc-toc
|
|
24
|
+
- previous_group = nil
|
|
25
|
+
- @records.each do |record|
|
|
26
|
+
- group = @config.group_label.call(record)
|
|
27
|
+
- if group && group != previous_group
|
|
28
|
+
%li.howdoc-toc-group= group
|
|
29
|
+
- previous_group = group
|
|
30
|
+
- current = @current && record.filename == @current.filename
|
|
31
|
+
%li.howdoc-toc-entry{ class: ('howdoc-current' if current) }
|
|
32
|
+
%a{ href: "./#{record.filename}", 'aria-current': (current ? 'page' : nil) }= record.heading
|
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.
|
|
4
|
+
version: 0.5.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
|