studio-engine 0.85.0 → 0.86.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 +51 -0
- data/README.md +8 -0
- data/app/controllers/registrations_controller.rb +3 -1
- data/app/helpers/studio/site_footer_helper.rb +9 -0
- data/app/views/registrations/new.html.erb +58 -35
- data/app/views/studio/site_footer/_assets.html.erb +27 -159
- data/app/views/studio/site_footer/_map.html.erb +4 -4
- data/app/views/studio/site_footer/_map_assets.html.erb +157 -0
- data/docs/BOOKING.md +377 -0
- data/docs/SITE_FOOTER.md +375 -0
- data/lib/studio/engine.rb +81 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +32 -0
- data/studio-engine.gemspec +8 -1
- metadata +4 -1
data/docs/BOOKING.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Booking
|
|
2
|
+
|
|
3
|
+
An app on studio-engine can take bookings through a Google Calendar appointment
|
|
4
|
+
schedule: inline in a page, in a popup, and on a `/schedule` page. The gem ships
|
|
5
|
+
the frame, the popup, the page and their scripts. Google does the scheduling.
|
|
6
|
+
|
|
7
|
+
It stands alone. An app does not need the [site footer](SITE_FOOTER.md) to use
|
|
8
|
+
it, and not every app needs it at all: it is a simple scheduler for an app that
|
|
9
|
+
wants one. Everything about *when* someone can book is configured in Google, not
|
|
10
|
+
here: availability, conflict checking across several calendars, buffers between
|
|
11
|
+
appointments, the booking form's questions, and the Meet link on the invitation.
|
|
12
|
+
|
|
13
|
+
## Adopt it
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# config/initializers/studio.rb
|
|
17
|
+
Studio.configure do |config|
|
|
18
|
+
config.booking_url = "https://calendar.google.com/calendar/appointments/schedules/EXAMPLE-SCHEDULE-ID"
|
|
19
|
+
config.draw_booking_routes = true # GET /schedule, the engine's booking page
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That is a working booking page at `/schedule`, in the app's own layout. To put
|
|
24
|
+
the calendar in a page of your own instead, or as well:
|
|
25
|
+
|
|
26
|
+
```erb
|
|
27
|
+
<%= studio_booking_frame %>
|
|
28
|
+
<%= studio_booking_link "Book a call", class: "btn btn-primary" %>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Nothing is added to the app's asset manifest or its Tailwind build. An app that
|
|
32
|
+
enforces a Content Security Policy must allow the frame: see
|
|
33
|
+
[Content Security Policy](#content-security-policy).
|
|
34
|
+
|
|
35
|
+
## Set up the schedule in Google
|
|
36
|
+
|
|
37
|
+
These steps are Google Calendar's, written from memory and not checked against
|
|
38
|
+
Google's menus: the names and their order may differ from what you see. The
|
|
39
|
+
things to end up with are what matter.
|
|
40
|
+
|
|
41
|
+
1. **Create the appointment schedule.** In Google Calendar, signed in as the
|
|
42
|
+
account that will own the bookings: *Create*, then *Appointment schedule*.
|
|
43
|
+
Give it a title, an appointment length and its general availability. Under
|
|
44
|
+
the booked-appointment settings, set the buffer between appointments and the
|
|
45
|
+
most bookings in a day. Under the booking page's settings, choose Google Meet
|
|
46
|
+
as the place if each invitation should carry a Meet link, and choose what
|
|
47
|
+
the form asks for.
|
|
48
|
+
2. **Share the other calendars in, for conflict checking.** A schedule can only
|
|
49
|
+
avoid events it can see. For each other calendar that should block a slot (a
|
|
50
|
+
personal account, a second Workspace), open that calendar's settings in the
|
|
51
|
+
account that owns it, share it with the schedule's owner, and let the owner
|
|
52
|
+
see at least whether the time is free or busy. Then, in the schedule's
|
|
53
|
+
availability settings, tick those calendars under the ones checked for
|
|
54
|
+
conflicts. Google limits checking more than one calendar to some paid
|
|
55
|
+
plans; check what the owning account has.
|
|
56
|
+
3. **Copy the booking URL.** Open the schedule's booking page and take its long
|
|
57
|
+
address, the one of this shape:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
https://calendar.google.com/calendar/appointments/schedules/EXAMPLE-SCHEDULE-ID
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Google's *Share* dialog may also offer a short link; use the long address,
|
|
64
|
+
which is the one that embeds (the dialog's website-embed code carries it as the
|
|
65
|
+
frame's `src`). Paste it without `?gv=true`: the engine adds that for the
|
|
66
|
+
embedded page and leaves it off the link to Google's own page. A link pasted
|
|
67
|
+
with it is accepted and stored without it.
|
|
68
|
+
|
|
69
|
+
The id in that address is not a secret (it is on every page that embeds it), but
|
|
70
|
+
it is the app's own: keep real ids out of examples and tests.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
| Setting | What it is | Default |
|
|
75
|
+
|---------|------------|---------|
|
|
76
|
+
| `config.booking_url` | The schedule's public `https` address | `nil`: no booking. Every helper below renders nothing and no link opens a popup |
|
|
77
|
+
| `config.booking_crop` | The frame's crop at rest: `{ top:, bottom:, frame_height: }` | `nil`: the whole frame. See [The crop](#the-crop) |
|
|
78
|
+
| `config.draw_booking_routes` | Draw the engine's booking page, `GET /schedule` | `false` |
|
|
79
|
+
| `config.booking_path` | Where the app's OWN booking page lives: a path, or a callable receiving the view | `nil`. See [A page of your own](#a-page-of-your-own) |
|
|
80
|
+
|
|
81
|
+
## Helpers
|
|
82
|
+
|
|
83
|
+
| Helper | What it renders |
|
|
84
|
+
|--------|-----------------|
|
|
85
|
+
| `studio_booking_frame` | Google's booking page inline, plus a line linking to the page itself. `title:` names the frame; `crop:` is `true` (the default: use `config.booking_crop`), `false`, or a Hash for this frame alone |
|
|
86
|
+
| `studio_booking_link "Book a call", class: "btn btn-primary"` | A link that opens the popup in place. Its href is the booking page, so it works with scripts off |
|
|
87
|
+
| `studio_booking_popup` | The dialog. The site footer renders it; call it yourself on a page with booking links and no footer |
|
|
88
|
+
|
|
89
|
+
## The frame
|
|
90
|
+
|
|
91
|
+
- **Deferred.** The URL waits in `data-src`. A script assigns `src` only after the
|
|
92
|
+
window's `load` event, and only once the frame is within 200px of the viewport.
|
|
93
|
+
`loading="lazy"` is not enough: a third-party frame that starts before `load`
|
|
94
|
+
holds that event open for as long as Google takes to answer.
|
|
95
|
+
- **Whole at rest, unless the app declares a crop.** Without one the frame shows
|
|
96
|
+
Google's whole page, 732px tall from 640px up and 1200px below that, where
|
|
97
|
+
Google stacks the month above the slots.
|
|
98
|
+
- **White in both themes.** Google's page cannot be themed.
|
|
99
|
+
- **With scripts off** the frame is replaced by a plain link to the booking page.
|
|
100
|
+
|
|
101
|
+
## The crop
|
|
102
|
+
|
|
103
|
+
Google's page has a header above the calendar (the owner, the title, the length,
|
|
104
|
+
the meeting line) and credit lines below it. A crop hides both at rest, so the
|
|
105
|
+
page shows only the "Select an appointment time" box, and opens the frame to its
|
|
106
|
+
full height when the visitor starts using it.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
config.booking_crop = { top: 200, bottom: 600, frame_height: 720 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The three numbers are pixels in Google's own page, at 640px wide or more:
|
|
113
|
+
|
|
114
|
+
| Key | What it is | Does it move? |
|
|
115
|
+
|-----|------------|---------------|
|
|
116
|
+
| `top` | The box's top edge | Only when the schedule's header changes: its title, description or meeting line |
|
|
117
|
+
| `bottom` | The box's bottom edge **on its fullest day** | Yes. See below |
|
|
118
|
+
| `frame_height` | The whole page's height on that fullest day | With `bottom` |
|
|
119
|
+
|
|
120
|
+
**There is no default crop, because the numbers belong to one schedule.** Two
|
|
121
|
+
schedules with different headers have different tops, and a crop measured on one
|
|
122
|
+
slices the other's heading. An app that has not measured gets the whole frame.
|
|
123
|
+
|
|
124
|
+
### How the window is derived
|
|
125
|
+
|
|
126
|
+
The wrapper shows `top - 6` to `bottom + 6`, held inside the frame, and the frame
|
|
127
|
+
behind it is `frame_height` tall. Six pixels is the margin 0.83.0 used.
|
|
128
|
+
|
|
129
|
+
The box is as tall as the month grid or the longest column of appointment slots,
|
|
130
|
+
whichever is taller, at 48px a slot. The month grid was six rows in every month
|
|
131
|
+
measured (two schedules, seven months each, on 2026-10-01), so a short month did
|
|
132
|
+
not move the box. The slot column is not fixed: a day that is partly booked,
|
|
133
|
+
or partly over, has fewer slots than a free one. So **the box's bottom on any
|
|
134
|
+
one day is not the schedule's
|
|
135
|
+
bottom**, and a crop measured from one look at the page cuts off the last slots
|
|
136
|
+
of a fuller day.
|
|
137
|
+
|
|
138
|
+
That is why `bottom` is the bottom on the fullest day, and why it cannot slice
|
|
139
|
+
content: on every other day the box is shorter than the window, and what shows
|
|
140
|
+
under it is a strip of Google's credit lines. The crop errs toward showing too
|
|
141
|
+
much, never too little. `frame_height` is the page's height on that same day, so
|
|
142
|
+
the opened frame holds the whole page without a scrollbar of its own.
|
|
143
|
+
|
|
144
|
+
What the crop cannot follow is a change to the schedule itself. Measure again
|
|
145
|
+
when the header changes, or when a day can hold more slots than the fullest day
|
|
146
|
+
measured (longer hours, shorter appointments).
|
|
147
|
+
|
|
148
|
+
### Measure it
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm ci && npx playwright install chromium # once
|
|
152
|
+
bin/booking-crop-measure https://calendar.google.com/calendar/appointments/schedules/EXAMPLE-SCHEDULE-ID
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Run it from a checkout of this repo. It opens Google's embedded page in headless
|
|
156
|
+
Chromium at two frame widths (640px and 862px), selects every bookable day of
|
|
157
|
+
the next three months (`--months N` for more), and prints the Hash, with what it
|
|
158
|
+
measured against:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
640px wide: box 200-600px, page 720px tall.
|
|
162
|
+
month grid: 6 rows. Fullest day: 8 slot rows (Tuesday, January 8, 2030).
|
|
163
|
+
walked 28 bookable days over 3 month(s). As it opened: box bottom 456px, 5 slot rows.
|
|
164
|
+
...
|
|
165
|
+
config.booking_crop = { top: 200, bottom: 600, frame_height: 720 }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
It books nothing: it only selects days. It reads Google's page by role and
|
|
169
|
+
attribute (`main`, `table[role=grid]`, `td[data-date]`, `[role=list]`), and stops
|
|
170
|
+
with an error if the page is no longer shaped that way, rather than print numbers
|
|
171
|
+
from the wrong element. `--json` prints the measurements for a script.
|
|
172
|
+
|
|
173
|
+
It reaches the live Google page, so it never runs in CI: the command refuses
|
|
174
|
+
under a `CI` environment, and the browser lane proves the measuring against a
|
|
175
|
+
stub (`e2e/booking_crop_measure.spec.js`).
|
|
176
|
+
|
|
177
|
+
### One frame's own crop
|
|
178
|
+
|
|
179
|
+
```erb
|
|
180
|
+
<%= studio_booking_frame crop: false %> <%# whole, whatever the app declares %>
|
|
181
|
+
<%= studio_booking_frame crop: { top: 150, bottom: 480, frame_height: 640 } %> <%# this frame alone %>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each cropped wrapper carries its numbers as custom properties
|
|
185
|
+
(`--booking-crop-offset`, `--booking-crop-window`, `--booking-frame-height`) in
|
|
186
|
+
its own `style` attribute, so two frames with different crops can share a page.
|
|
187
|
+
Each opens when focus moves into it.
|
|
188
|
+
|
|
189
|
+
### A crop that cannot be one
|
|
190
|
+
|
|
191
|
+
A Hash whose values are not numbers (a numeric string such as `"200"` is not
|
|
192
|
+
coerced: it is refused like any other non-number), a negative `top`, a `bottom` that is not
|
|
193
|
+
below `top` or is past `frame_height`, or a window as tall as the frame, is not
|
|
194
|
+
applied. The frame shows whole, and one line is logged:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
[studio.booking] booking crop {...} is ignored (bottom is not below top); the frame shows whole.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`config.booking_crop` logs it when it is assigned, at boot. Anything that is not
|
|
201
|
+
a Hash, `nil` or `false` raises there.
|
|
202
|
+
|
|
203
|
+
### Its limits
|
|
204
|
+
|
|
205
|
+
- **The crop is off below 640px**, where Google stacks the month above the slots.
|
|
206
|
+
- **The breakpoint is the viewport's, not the frame's.** Google lays the month
|
|
207
|
+
beside the slots only when the frame itself is about 600px wide. A frame in a
|
|
208
|
+
narrow column on a wide screen is stacked, and a crop measured side by side
|
|
209
|
+
does not fit it: pass `crop: false` there.
|
|
210
|
+
- **The crop cannot stay on.** The form Google shows after a slot is picked is a
|
|
211
|
+
dialog centred in the frame's full height, so the frame opens fully on use.
|
|
212
|
+
|
|
213
|
+
## The popup
|
|
214
|
+
|
|
215
|
+
A booking link opens Google's page in a `<dialog>` without leaving the page. The
|
|
216
|
+
frame inside is requested the first time the dialog opens, and kept while that
|
|
217
|
+
page stays up: closing and reopening does not ask Google again. The popup is
|
|
218
|
+
never cropped.
|
|
219
|
+
|
|
220
|
+
A Turbo restoration visit (back or forward) is a new page built from a snapshot,
|
|
221
|
+
and it does ask again. The inline frame is restored with its `src`, so the
|
|
222
|
+
browser re-requests Google's page for it. The popup's frame is not: its `src` is
|
|
223
|
+
put back to waiting before the snapshot is taken, so a restored page asks Google
|
|
224
|
+
for the popup only when someone opens it.
|
|
225
|
+
|
|
226
|
+
- **On a page that already shows the inline frame**, a booking link scrolls to
|
|
227
|
+
that frame, opens its crop and moves focus into it. It does not open a second
|
|
228
|
+
calendar on top of the first.
|
|
229
|
+
- **The link's href is the fallback**: with scripts off, on a modified click (new
|
|
230
|
+
tab), or on a page with no dialog, it is an ordinary link. It goes to the
|
|
231
|
+
app's booking page when there is one, else to Google's own page.
|
|
232
|
+
- **The page behind does not scroll** while the dialog is open.
|
|
233
|
+
- **It is sized to the visible viewport** (`dvh`, with `vh` as the fallback), so
|
|
234
|
+
a phone's toolbars do not push the Close button off screen.
|
|
235
|
+
|
|
236
|
+
It is a native `<dialog>`, not the engine's modal host (`studio/modals/_host`),
|
|
237
|
+
on purpose. The host needs Alpine, its store, and each modal registered inside
|
|
238
|
+
the host's block in the layout, so an app without a host would have no popup.
|
|
239
|
+
The host's card is padded and scrolls, where this body is a full-bleed frame. And
|
|
240
|
+
the host mounts content with `<template x-if>`, which would build a new frame and
|
|
241
|
+
ask Google again on every open.
|
|
242
|
+
|
|
243
|
+
## The page
|
|
244
|
+
|
|
245
|
+
`config.draw_booking_routes = true` draws `GET /schedule`
|
|
246
|
+
(`Studio::BookingsController`, helper `studio_booking_path`): a heading, the
|
|
247
|
+
frame with the app's crop, and the footer if the app has one, in the app's own
|
|
248
|
+
layout. It is public, and answers `404` when no `booking_url` is set.
|
|
249
|
+
|
|
250
|
+
The route is a flag, off by default, like every route a consumer might already
|
|
251
|
+
own.
|
|
252
|
+
|
|
253
|
+
### A page of your own
|
|
254
|
+
|
|
255
|
+
An app that wants different words around the calendar renders
|
|
256
|
+
`studio_booking_frame` in a view of its own. The frame is the same one, crop
|
|
257
|
+
included, and needs nothing else. Tell the engine where that page is:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
config.booking_path = "/schedule"
|
|
261
|
+
# or, with a route helper:
|
|
262
|
+
config.booking_path = ->(view) { view.schedule_path }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
With it set the app's page is treated as the engine's own is:
|
|
266
|
+
|
|
267
|
+
- a site-footer link whose href is that path opens the popup without
|
|
268
|
+
`{ booking: true }`;
|
|
269
|
+
- `studio_booking_link` falls back to it, instead of to Google's page;
|
|
270
|
+
- a signed-in viewer keeps the footer on it, without the controller being listed
|
|
271
|
+
in `config.site_footer_controllers`. The page is matched by the request's path.
|
|
272
|
+
|
|
273
|
+
The path is written into hrefs, so it must be a local path: one leading `/`.
|
|
274
|
+
A String that is anything else (`javascript:...`, `https://...`, a
|
|
275
|
+
protocol-relative `//host/...`) raises when it is assigned. A callable's result
|
|
276
|
+
is held to the same rule on every request: anything else is logged once
|
|
277
|
+
(`[studio.booking] booking_path ... is ignored`) and treated as no answer.
|
|
278
|
+
|
|
279
|
+
Without `booking_path` (and without the engine's route) an app's own page gets
|
|
280
|
+
none of the three: the engine has no way to know it is the booking page. If both
|
|
281
|
+
are declared, `booking_path` is the booking page. A callable that returns `nil`
|
|
282
|
+
falls back to the engine's page.
|
|
283
|
+
|
|
284
|
+
## With the site footer
|
|
285
|
+
|
|
286
|
+
The footer renders the popup, and prints a booking link in any column that names
|
|
287
|
+
one (see [`SITE_FOOTER.md`](SITE_FOOTER.md)). Its facts may carry the copy:
|
|
288
|
+
`booking: { label:, title: }` sets what the links, the popup and the page call
|
|
289
|
+
the act ("Schedule a call") and the frame's accessible name. Without a footer the
|
|
290
|
+
label is "Schedule a call" and the name is "Schedule a call with <site title>".
|
|
291
|
+
|
|
292
|
+
## Content Security Policy
|
|
293
|
+
|
|
294
|
+
An app that enforces a policy must allow the frame:
|
|
295
|
+
|
|
296
|
+
| Directive | Source | For |
|
|
297
|
+
|-----------|--------|-----|
|
|
298
|
+
| `frame-src` | `https://calendar.google.com` | The booking frame and the popup |
|
|
299
|
+
|
|
300
|
+
The primitives' own `<style>` and `<script>` blocks are inline and carry no
|
|
301
|
+
nonce, like the rest of the engine's partials, and a cropped wrapper carries its
|
|
302
|
+
numbers in an inline `style` attribute. A policy that forbids inline script or
|
|
303
|
+
style needs `'unsafe-inline'` for them (or hashes), exactly as it does for the
|
|
304
|
+
engine's head partial today.
|
|
305
|
+
|
|
306
|
+
## Limits
|
|
307
|
+
|
|
308
|
+
- **Google's page cannot be themed.** It is another origin's document in a
|
|
309
|
+
frame: its colours, type and wording are Google's. The frame sits on a white
|
|
310
|
+
panel in both themes.
|
|
311
|
+
- **Escape does not close the popup once focus is inside the frame.** Key
|
|
312
|
+
presses there belong to Google, and nothing in the page can change that. The
|
|
313
|
+
Close button and a click on the backdrop always work, which is why Close is a
|
|
314
|
+
solid, full-size button.
|
|
315
|
+
- **The page cannot see into the frame.** It is told nothing about a click or a
|
|
316
|
+
booking. The one signal it gets is that focus moved into the frame, which is
|
|
317
|
+
what opens a crop.
|
|
318
|
+
- **A crop is a measurement of Google's layout.** If Google redesigns the page,
|
|
319
|
+
measure again, or set `config.booking_crop = nil`.
|
|
320
|
+
|
|
321
|
+
## Upgrading from 0.83.0
|
|
322
|
+
|
|
323
|
+
**0.83.0 cropped every frame, with one schedule's numbers. The next release
|
|
324
|
+
crops none until the app declares its own.** After upgrading, a frame that was
|
|
325
|
+
cropped shows Google's whole page.
|
|
326
|
+
|
|
327
|
+
To keep exactly the crop 0.83.0 drew (a 414px window, 205px down a 732px frame):
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
config.booking_crop = { top: 211, bottom: 613, frame_height: 732 }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Prefer to measure. Those numbers were read from one schedule with five slots
|
|
334
|
+
showing (613 is that box's bottom). Measured again on 2026-10-01, the same
|
|
335
|
+
schedule's fullest day has eight, so the 0.83.0 window cuts off its last three;
|
|
336
|
+
and on a schedule with a different header it slices the heading.
|
|
337
|
+
`bin/booking-crop-measure` gives the numbers for yours.
|
|
338
|
+
|
|
339
|
+
`studio_booking_frame crop: false` still means the whole frame. `crop: true`,
|
|
340
|
+
the default, now means "the app's crop", which is none until declared.
|
|
341
|
+
|
|
342
|
+
An app that renders its own booking page should also set `config.booking_path`
|
|
343
|
+
(above); 0.83.0 had no way to name it.
|
|
344
|
+
|
|
345
|
+
## Coexisting with an app's own copy
|
|
346
|
+
|
|
347
|
+
An app that still renders local booking partials uses the same `data-booking-*`
|
|
348
|
+
attributes. The engine's scripts keep out of its way: their guards are engine
|
|
349
|
+
names (`__studioBookingFramesArmed`, `__studioBookingPopupArmed`), and they act
|
|
350
|
+
only on engine-rendered elements, which carry `data-studio-booking` (the frame,
|
|
351
|
+
its wrapper, the dialog and the links). Write a booking link with
|
|
352
|
+
`studio_booking_link`, not by hand, so it carries the marker.
|
|
353
|
+
|
|
354
|
+
## Markup hooks
|
|
355
|
+
|
|
356
|
+
Stable, for tests and for an app's own scripts: `[data-booking-wrap]` (also
|
|
357
|
+
`[data-booking-crop]` and `.booking-frame-cropped` when cropped, `.is-open` once
|
|
358
|
+
used), `iframe[data-booking-frame]`, `dialog[data-booking-dialog]`,
|
|
359
|
+
`iframe[data-booking-popup-frame]`, `a[data-booking-popup]`,
|
|
360
|
+
`[data-booking-close]`, `[data-booking-page]`. They also carry
|
|
361
|
+
`data-studio-booking`.
|
|
362
|
+
|
|
363
|
+
## Tests
|
|
364
|
+
|
|
365
|
+
- `test/lib/studio/booking_test.rb`: the crop's derivation and what it refuses,
|
|
366
|
+
and the booking page's path. `test/lib/studio/site_footer_test.rb` holds the
|
|
367
|
+
booking URLs.
|
|
368
|
+
- `test/integration/site_footer_test.rb`: what a host renders: whole by default,
|
|
369
|
+
the declared crop, one frame's own, the engine's page, and an app's own page
|
|
370
|
+
named by `booking_path`.
|
|
371
|
+
- `e2e/booking_frame.spec.js`: the scripts and the layout in a browser, with
|
|
372
|
+
Google stubbed: deferred loading, whole by default, cropped to declared numbers
|
|
373
|
+
and open on use, two crops on one page, the popup.
|
|
374
|
+
- `e2e/booking_crop_measure.spec.js`: the measuring tool's walk and arithmetic,
|
|
375
|
+
against a stub.
|
|
376
|
+
|
|
377
|
+
`/admin/style` shows the frame's two states under Tricks.
|