Archived
Catering, and a pure Java/Spring site: Thymeleaf front to back #10
@@ -0,0 +1,126 @@
|
||||
-- The catering tables in the bakery's voice instead of the spreadsheet's shorthand.
|
||||
--
|
||||
-- V4 kept the source wording deliberately: it was a spreadsheet written for the people who bake from it,
|
||||
-- and inventing copy for somebody else's prices is not a migration's job. But the page is read by
|
||||
-- customers, and "B&G cake", "2 pans", "4 dz" and "6+6" are notes-to-self, not an offer. So this rewrites
|
||||
-- the cells as sentences, fills in the blurb each table always had room for, and merges the parties
|
||||
-- lines the same way V5 merged the wedding ones — "Cupcakes" quantified as "1 dz sugar cookies or
|
||||
-- cupcakes" with an empty "Sugar cookies" line beneath it is the same offset, and the same fix.
|
||||
--
|
||||
-- Guarded on the exact text V4 and V5 left, as V5 was: anything the bakery has already reworded in the
|
||||
-- admin is left exactly as they wrote it. Their wording wins over mine.
|
||||
|
||||
-- --- names and the line under each heading ----------------------------------------------------------
|
||||
|
||||
update catering_package set name = 'Office boxes'
|
||||
where name = 'Office';
|
||||
|
||||
update catering_package set blurb = 'Mini pastries for a morning meeting, boxed and ready to collect.'
|
||||
where name = 'Office boxes' and blurb is null;
|
||||
|
||||
update catering_package set blurb = 'A cake, and something to hand round, sized to your guest list.'
|
||||
where name = 'Parties' and blurb is null;
|
||||
|
||||
update catering_package set blurb = 'A cake for the couple, and dessert for everyone else.'
|
||||
where name = 'Weddings' and blurb is null;
|
||||
|
||||
-- --- office boxes -----------------------------------------------------------------------------------
|
||||
|
||||
update catering_row_value v set value = 'A dozen'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Mini muffins' and v.value = '12 items';
|
||||
update catering_row_value v set value = 'Eighteen'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Mini muffins' and v.value = '18 items';
|
||||
update catering_row_value v set value = 'Two dozen'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Mini muffins' and v.value = '24 items';
|
||||
|
||||
-- "6+6" is six of one flavour and six of another, which is worth saying out loud.
|
||||
update catering_row_value v set value = 'A dozen, in two flavors'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Mini scones' and v.value = '6+6';
|
||||
update catering_row_value v set value = 'Eighteen, in two or three flavors'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Mini scones' and v.value = '6+6+6 or 12+6';
|
||||
update catering_row_value v set value = 'Two dozen, in up to four flavors'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Mini scones'
|
||||
and v.value = '6+6+6+6 or 12+6+6 or 12+12';
|
||||
|
||||
update catering_package_note set body = 'Everything is baked in sixes, so each item comes in multiples of six.'
|
||||
where body = 'Minimum of 6 items per baked good. Flavors can''t be mixed and matched unless you''re ordering a large quantity.';
|
||||
|
||||
insert into catering_package_note (package_id, position, body)
|
||||
select p.id, 1, 'Mixing flavors within one item needs a larger order — ask us and we will tell you.'
|
||||
from catering_package p
|
||||
where p.name = 'Office boxes'
|
||||
and not exists (select 1 from catering_package_note n where n.package_id = p.id and n.position = 1);
|
||||
|
||||
-- --- parties ----------------------------------------------------------------------------------------
|
||||
|
||||
update catering_row_value v set value = 'A 6-inch cake'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cake' and v.value = '6 in cake';
|
||||
update catering_row_value v set value = 'An 8-inch cake'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cake' and v.value = '8 in cake';
|
||||
update catering_row_value v set value = 'A 10-inch cake'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cake' and v.value = '10 in cake';
|
||||
|
||||
-- Same offset as the wedding table: the quantities on the "Cupcakes" line were always
|
||||
-- cupcakes-or-sugar-cookies, and the "Sugar cookies" line below carried nothing at all.
|
||||
update catering_row r set label = 'Cupcakes or sugar cookies'
|
||||
from catering_package p
|
||||
where p.id = r.package_id and p.name = 'Parties' and r.label = 'Cupcakes';
|
||||
|
||||
update catering_row_value v set value = 'A dozen, either one'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies'
|
||||
and v.value = '1 dz sugar cookies or cupcakes';
|
||||
update catering_row_value v set value = 'Eighteen, your choice'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies'
|
||||
and v.value = '1.5 dz your choice';
|
||||
update catering_row_value v set value = 'Two dozen, your choice'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies'
|
||||
and v.value = '2 dz your choice';
|
||||
|
||||
delete from catering_row r
|
||||
using catering_package p
|
||||
where p.id = r.package_id and p.name = 'Parties' and r.label = 'Sugar cookies'
|
||||
and not exists (select 1 from catering_row_value v where v.row_id = r.id and v.value <> '');
|
||||
|
||||
update catering_package_note set body = 'An extra dozen is $20.'
|
||||
where body = 'Add an extra dozen for $20.';
|
||||
update catering_package_note
|
||||
set body = 'Cake and cupcake flavors can be different once you are ordering a dozen cupcakes or more.'
|
||||
where body = 'Cake and cupcake flavors can''t be mixed unless you order at least 1 dz of cupcakes.';
|
||||
|
||||
-- --- weddings ---------------------------------------------------------------------------------------
|
||||
|
||||
-- The label said it and the cell repeated it. The cell now carries the size instead.
|
||||
update catering_row r set label = 'Bride & groom cake'
|
||||
from catering_package p
|
||||
where p.id = r.package_id and p.name = 'Weddings' and r.label = 'Bride & groom cake (8 in)';
|
||||
|
||||
update catering_row_value v set value = 'Eight inch'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Bride & groom cake' and v.value = 'B&G cake';
|
||||
|
||||
update catering_row r set label = 'Sheet cakes or 12×17 bars'
|
||||
from catering_package p
|
||||
where p.id = r.package_id and p.name = 'Weddings' and r.label = 'Sheet cakes or 12x17 bars';
|
||||
|
||||
update catering_row_value v set value = 'Two pans'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Sheet cakes or 12×17 bars' and v.value = '2 pans';
|
||||
update catering_row_value v set value = 'Three pans'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Sheet cakes or 12×17 bars' and v.value = '3 pans';
|
||||
update catering_row_value v set value = 'Four pans'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Sheet cakes or 12×17 bars' and v.value = '4 pans';
|
||||
|
||||
update catering_row_value v set value = 'Four dozen'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' and v.value = '4 dz';
|
||||
update catering_row_value v set value = 'Five dozen'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' and v.value = '5 dz';
|
||||
update catering_row_value v set value = 'Six dozen'
|
||||
from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' and v.value = '6 dz';
|
||||
|
||||
-- --- the page's own terms ---------------------------------------------------------------------------
|
||||
|
||||
update catering_note
|
||||
set body = 'These are a guide rather than a menu. We are happy to make changes, though the price may change with them.'
|
||||
where body = 'We''re happy to make changes — the price may change with them.';
|
||||
|
||||
update catering_note
|
||||
set body = 'If we can''t do something, we will tell you.'
|
||||
where body = 'If we can''t do something we''ll tell you. These tables are mostly here to give you an idea of what''s possible.';
|
||||
@@ -4,9 +4,9 @@
|
||||
<div th:fragment="content">
|
||||
|
||||
<!--/*
|
||||
A short photographic band rather than the plain cream header the other pages use. This is the one
|
||||
page that is selling something, and the same treatment as the homepage hero — blurred photo, sage
|
||||
wash — carries the brand rather than inventing a new look for it.
|
||||
A short photographic band rather than the plain cream header the other pages use. This is the one page
|
||||
that is selling something, and the same treatment as the homepage hero — blurred photo, sage wash —
|
||||
carries the brand rather than inventing a new look for it.
|
||||
*/-->
|
||||
<section class="relative flex items-center py-14 md:py-20 bg-bakery-900 text-white overflow-hidden">
|
||||
<img th:src="${photos.of('gallery/Cakes.webp')}" alt=""
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div class="relative container mx-auto px-4 w-full min-w-0">
|
||||
<div class="max-w-3xl mx-auto text-center">
|
||||
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">To take away</p>
|
||||
<p class="text-xs sm:text-sm uppercase tracking-[0.2em] text-bakery-200">Order ahead</p>
|
||||
<h1 class="mt-3 font-adbhashitha text-4xl md:text-5xl text-bakery-50">Goodie boxes & catering</h1>
|
||||
<p class="mt-5 text-base sm:text-lg text-bakery-100 text-balance leading-relaxed">
|
||||
Boxes for the office, packages for a party, and cakes and desserts for a wedding.
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<p class="max-w-2xl mx-auto text-center text-bakery-800 leading-relaxed">
|
||||
Every one of these is a starting point. Tell us the date, the number of people and what you had in
|
||||
mind, and we will work from it.
|
||||
mind, and we will bake to it.
|
||||
</p>
|
||||
|
||||
<!--/* The branch from the wordmark, as a divider. It is a mask over currentColor, so it takes the
|
||||
@@ -39,107 +39,80 @@
|
||||
<span class="mark mark-r w-12 h-12 text-bakery-400"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 max-w-5xl mx-auto space-y-10 md:space-y-14">
|
||||
<div class="mt-12 max-w-6xl mx-auto space-y-16 md:space-y-20">
|
||||
|
||||
<!--/*
|
||||
One card per table, and each table is rendered twice from the same model with only one visible:
|
||||
One block per table, and each size within it is a card: the price, then what you get for it.
|
||||
|
||||
- A real <table> from `md` up. These are prices in columns; a table is what that is, and a screen
|
||||
reader announces the size and the item together because of the row and column headers.
|
||||
- Stacked cards below `md`. A four-column price table on a phone is either an illegible squeeze
|
||||
or a sideways scroll, and this page is mostly read on phones.
|
||||
This replaced a real <table>. A table is the honest shape for a price matrix, and it is how the
|
||||
bakery keeps these — but it is also exactly what the source spreadsheet looks like, and reading a
|
||||
spreadsheet back to a customer is not an offer. Cards say "here is what you get" three times over
|
||||
instead of asking someone to find the cell where a row meets a column, and they need no separate
|
||||
phone rendering, because a stack of cards IS the phone rendering.
|
||||
|
||||
Both walk the same rows, so they cannot drift.
|
||||
Each card walks the same rows in the same order and takes the cell for its own column — the
|
||||
alignment the aggregate guarantees.
|
||||
*/-->
|
||||
<section th:each="table, tbl : ${menu.packages}"
|
||||
class="bg-white rounded-3xl shadow-xs overflow-hidden">
|
||||
|
||||
<header class="px-6 pt-7 pb-5 md:px-10 md:pt-8 text-center border-b border-bakery-100">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl text-bakery-900" th:text="${table.name}">Office</h2>
|
||||
<p th:if="${table.blurb}" class="mt-2 text-bakery-700 max-w-xl mx-auto" th:text="${table.blurb}">Blurb</p>
|
||||
<section th:each="table : ${menu.packages}">
|
||||
<header class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="font-adbhashitha text-3xl md:text-4xl text-bakery-900" th:text="${table.name}">Office boxes</h2>
|
||||
<p th:if="${table.blurb}" class="mt-3 text-bakery-700 leading-relaxed" th:text="${table.blurb}">
|
||||
Mini pastries for a morning meeting.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!--/* Wide: the table. The header band carries the price; the rows are what you get for it. */-->
|
||||
<div class="hidden md:block px-6 lg:px-10 py-2">
|
||||
<table class="w-full border-collapse text-left">
|
||||
<caption class="sr-only" th:text="|${table.name} — what each size includes and what it costs|">Sizes</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<!--/* A blank corner: the caption above says what the table is, and every row names
|
||||
itself, so a label here only competed with the prices. */-->
|
||||
<th scope="col" class="w-52 py-5 pr-6"><span class="sr-only">What you get</span></th>
|
||||
<th th:each="tier : ${table.tiers}" scope="col"
|
||||
class="py-6 px-4 text-center align-bottom border-b-2 border-bakery-200">
|
||||
<span class="block text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</span>
|
||||
<span th:if="${tier.price}" class="mt-2 block font-adbhashitha text-3xl text-bakery-900"
|
||||
th:text="${tier.price}">$24</span>
|
||||
<!--/* No price means "ask us" — say so rather than leaving a hole in the column. */-->
|
||||
<span th:unless="${tier.price}" class="mt-2 block font-adbhashitha text-xl text-bakery-700">Ask us</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="row : ${table.rows}" class="align-top even:bg-bakery-50/60">
|
||||
<th scope="row" class="py-5 pr-6 pl-2 font-medium text-bakery-900" th:text="${row.label}">Mini muffins</th>
|
||||
<td th:each="value : ${row.values}" class="py-5 px-4 text-center text-bakery-800">
|
||||
<span th:if="${!#strings.isEmpty(value)}" th:text="${value}">12 items</span>
|
||||
<!--/* A cell the bakery hasn't filled in. A dash reads as "nothing here"; an empty cell
|
||||
reads as a broken page. */-->
|
||||
<span th:if="${#strings.isEmpty(value)}" class="text-bakery-400" aria-hidden="true">–</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--/* Three sizes is the common case; a table with one or two shouldn't stretch to fill the row. */-->
|
||||
<div class="mt-8 grid gap-6 md:gap-8"
|
||||
th:classappend="${#lists.size(table.tiers) == 1} ? 'max-w-sm mx-auto'
|
||||
: (${#lists.size(table.tiers) == 2} ? 'sm:grid-cols-2 max-w-3xl mx-auto'
|
||||
: 'sm:grid-cols-2 lg:grid-cols-3')">
|
||||
<article th:each="tier, t : ${table.tiers}"
|
||||
class="flex flex-col bg-white rounded-3xl shadow-xs p-6 md:p-8
|
||||
transition-shadow duration-300 hover:shadow-lg">
|
||||
<p class="text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</p>
|
||||
<p th:if="${tier.price}" class="mt-2 font-adbhashitha text-4xl text-bakery-900" th:text="${tier.price}">$24</p>
|
||||
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
|
||||
<p th:unless="${tier.price}" class="mt-2 font-adbhashitha text-2xl text-bakery-700">Ask us</p>
|
||||
|
||||
<!--/* Narrow: one card per size. */-->
|
||||
<div class="md:hidden px-5 py-6 space-y-4">
|
||||
<div th:each="tier, t : ${table.tiers}" class="rounded-2xl border border-bakery-200 overflow-hidden">
|
||||
<div class="flex items-baseline justify-between gap-3 bg-bakery-50 px-4 py-3">
|
||||
<h3 class="text-xs uppercase tracking-[0.15em] text-bakery-600" th:text="${tier.label}">Small</h3>
|
||||
<span th:if="${tier.price}" class="font-adbhashitha text-2xl text-bakery-900 whitespace-nowrap"
|
||||
th:text="${tier.price}">$24</span>
|
||||
<span th:unless="${tier.price}" class="font-adbhashitha text-lg text-bakery-700 whitespace-nowrap">Ask us</span>
|
||||
</div>
|
||||
<!--/* The cell for this size on each line — t.index picks this column out of every row, which
|
||||
is exactly the alignment the aggregate guarantees. Lines with nothing in this column are
|
||||
still listed: they are part of what's in the box, and the bakery just hasn't said how
|
||||
many yet. */-->
|
||||
<dl class="divide-y divide-bakery-100">
|
||||
<div th:each="row : ${table.rows}" class="flex justify-between gap-4 px-4 py-2.5 text-sm">
|
||||
<dt class="text-bakery-900" th:text="${row.label}">Mini muffins</dt>
|
||||
<dd class="text-bakery-800 text-right">
|
||||
<span th:if="${!#strings.isEmpty(row.values[t.index])}" th:text="${row.values[t.index]}">12 items</span>
|
||||
<span th:if="${#strings.isEmpty(row.values[t.index])}" class="text-bakery-400">–</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<ul class="mt-6 space-y-3 text-bakery-800">
|
||||
<li th:each="row : ${table.rows}" class="flex gap-3">
|
||||
<!--/* A tick, not a bullet: this is a list of what you get, not prose. */-->
|
||||
<svg class="mt-1 h-4 w-4 shrink-0 text-bakery-400" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true">
|
||||
<path d="M20 6L9 17l-5-5" />
|
||||
</svg>
|
||||
<span>
|
||||
<span th:text="${row.label}">Mini muffins</span>
|
||||
<!--/* A line the bakery hasn't quantified for this size still belongs in the box; it
|
||||
simply appears without a number, rather than as an empty cell in a grid. */-->
|
||||
<span th:if="${!#strings.isEmpty(row.values[t.index])}" class="text-bakery-600"
|
||||
th:text="|— ${row.values[t.index]}|">— a dozen</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!--/*
|
||||
The small print and the enquiry link share one tinted footer. As two more full-width bands
|
||||
they made every card read as four stacked stripes.
|
||||
The small print, then one link for the whole table.
|
||||
|
||||
The link is quiet rather than a filled button: three filled buttons down a page would shout,
|
||||
and the loud call to action belongs once, at the end.
|
||||
A button on every card was the obvious pricing-page move and it was wrong twice over: all
|
||||
three cards led to the same place, since an enquiry is about the table rather than the size,
|
||||
and the label read "Ask about the 15–20 people" on the tables whose sizes are headcounts.
|
||||
*/-->
|
||||
<div class="px-6 md:px-10 py-6 bg-bakery-50/70 border-t border-bakery-100
|
||||
flex flex-col gap-5 md:flex-row md:items-end md:justify-between">
|
||||
<div th:if="${!#lists.isEmpty(table.notes)}" class="md:max-w-2xl">
|
||||
<div class="mt-8 max-w-2xl mx-auto text-center">
|
||||
<div th:if="${!#lists.isEmpty(table.notes)}">
|
||||
<h3 class="text-xs uppercase tracking-[0.15em] text-bakery-500 font-medium">Good to know</h3>
|
||||
<ul class="mt-3 space-y-2 text-sm text-bakery-700 list-disc pl-5 marker:text-bakery-300">
|
||||
<li th:each="note : ${table.notes}" th:text="${note}">Minimum of 6 items per baked good.</li>
|
||||
<ul class="mt-3 space-y-2 text-sm text-bakery-700">
|
||||
<li th:each="note : ${table.notes}" th:text="${note}">Everything is baked in sixes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--/* One text run, so the underline is continuous: as flex children with a gap it came out
|
||||
as "Ask about" and "office" separately underlined. The arrow stays outside it. */-->
|
||||
<a th:href="|/contact?about=${#uris.escapeQueryParam(table.name)}|"
|
||||
class="shrink-0 text-bakery-700 hover:text-bakery-900 font-medium transition-colors whitespace-nowrap">
|
||||
<span class="underline underline-offset-4"
|
||||
th:text="|Ask about ${#strings.toLowerCase(table.name)}|">Ask about office</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
class="mt-6 inline-block border border-bakery-300 hover:bg-bakery-100 text-bakery-800
|
||||
px-7 py-3 rounded-full font-medium tracking-wide transition-colors"
|
||||
th:text="|Ask about ${#strings.toLowerCase(table.name)}|">Ask about office boxes</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -160,7 +133,7 @@
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h2 class="font-adbhashitha text-2xl md:text-3xl">Before you order</h2>
|
||||
<ul th:if="${!#lists.isEmpty(menu.notes)}" class="mt-6 space-y-3 text-bakery-100 leading-relaxed">
|
||||
<li th:each="note : ${menu.notes}" th:text="${note}">Prices may change.</li>
|
||||
<li th:each="note : ${menu.notes}" th:text="${note}">These are a guide rather than a menu.</li>
|
||||
</ul>
|
||||
<div class="mt-10 flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<a href="/contact"
|
||||
|
||||
@@ -128,7 +128,7 @@ class AdminPagesTest {
|
||||
// Indexed names are what let Spring bind the grid back into the right cells.
|
||||
.andExpect(content().string(containsString("name=\"columns[0].label\"")))
|
||||
.andExpect(content().string(containsString("name=\"lines[0].values[1]\"")))
|
||||
.andExpect(content().string(containsString("value=\"18 items\"")))
|
||||
.andExpect(content().string(containsString("value=\"Eighteen\"")))
|
||||
// The price round-trips as text: it came out "$24" and goes back the same way.
|
||||
.andExpect(content().string(containsString("value=\"$24\"")));
|
||||
}
|
||||
@@ -170,7 +170,7 @@ class AdminPagesTest {
|
||||
|
||||
// And nothing was written: the live page still says what it said.
|
||||
mvc.perform(get("/api/catering"))
|
||||
.andExpect(content().string(containsString("12 items")));
|
||||
.andExpect(content().string(containsString("A dozen")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -57,10 +57,12 @@ class CateringMenuTest {
|
||||
|
||||
@Test
|
||||
void carriesTheBakerysSpreadsheetIntoTheDatabase() {
|
||||
// The prices, the sizes and the shape are the spreadsheet's (V4). The wording is not: V6 rewrote
|
||||
// the cells as sentences a customer can read, since "6+6" and "4 dz" were notes to the baker.
|
||||
List<CateringMenu.PackageView> tables = catering.menu().packages();
|
||||
|
||||
assertThat(tables).extracting(CateringMenu.PackageView::name)
|
||||
.containsExactly("Office", "Parties", "Weddings");
|
||||
.containsExactly("Office boxes", "Parties", "Weddings");
|
||||
|
||||
CateringMenu.PackageView office = tables.get(0);
|
||||
assertThat(office.tiers()).extracting(CateringMenu.TierView::label)
|
||||
@@ -69,9 +71,10 @@ class CateringMenuTest {
|
||||
.containsExactly("$24", "$32", "$40");
|
||||
assertThat(office.rows()).extracting(CateringMenu.RowView::label)
|
||||
.containsExactly("Mini muffins", "Mini scones", "Mini cinnamon rolls");
|
||||
assertThat(office.rows().get(0).values()).containsExactly("12 items", "18 items", "24 items");
|
||||
assertThat(office.rows().get(1).values().get(2)).isEqualTo("6+6+6+6 or 12+6+6 or 12+12");
|
||||
assertThat(office.notes()).singleElement().asString().contains("Minimum of 6 items per baked good");
|
||||
assertThat(office.rows().get(0).values()).containsExactly("A dozen", "Eighteen", "Two dozen");
|
||||
assertThat(office.rows().get(1).values().get(2)).isEqualTo("Two dozen, in up to four flavors");
|
||||
assertThat(office.blurb()).contains("morning meeting");
|
||||
assertThat(office.notes()).anySatisfy(note -> assertThat(note).contains("baked in sixes"));
|
||||
|
||||
assertThat(tables.get(1).tiers()).extracting(CateringMenu.TierView::label)
|
||||
.containsExactly("15–20 people", "20–30 people", "30–40 people");
|
||||
@@ -79,6 +82,9 @@ class CateringMenuTest {
|
||||
.containsExactly("$236", "$310", "$386");
|
||||
// Wedding delivery terms belong to the wedding table, not to the page.
|
||||
assertThat(tables.get(2).notes()).anySatisfy(note -> assertThat(note).contains("delivery fee"));
|
||||
// No cell anywhere still speaks in shorthand.
|
||||
assertThat(tables).allSatisfy(table -> assertThat(table.rows()).allSatisfy(row ->
|
||||
assertThat(row.values()).noneMatch(value -> value.matches(".*\\b(dz|B&G|in cake)\\b.*"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,15 +96,28 @@ class CateringMenuTest {
|
||||
CateringMenu.PackageView weddings = catering.menu().packages().get(2);
|
||||
|
||||
assertThat(weddings.rows()).extracting(CateringMenu.RowView::label)
|
||||
.containsExactly("Bride & groom cake (8 in)", "Sheet cakes or 12x17 bars",
|
||||
.containsExactly("Bride & groom cake", "Sheet cakes or 12×17 bars",
|
||||
"Cupcakes or sugar cookies");
|
||||
assertThat(weddings.rows().get(1).values()).containsExactly("2 pans", "3 pans", "4 pans");
|
||||
assertThat(weddings.rows().get(2).values()).containsExactly("4 dz", "5 dz", "6 dz");
|
||||
assertThat(weddings.rows().get(1).values()).containsExactly("Two pans", "Three pans", "Four pans");
|
||||
assertThat(weddings.rows().get(2).values()).containsExactly("Four dozen", "Five dozen", "Six dozen");
|
||||
// Nothing left that is blank the whole way across.
|
||||
assertThat(weddings.rows())
|
||||
.noneMatch(row -> row.values().stream().allMatch(String::isEmpty));
|
||||
}
|
||||
|
||||
@Test
|
||||
void thePartyLinesEachSaySomethingToo() {
|
||||
// Same offset as the wedding table: the quantities on "Cupcakes" were always
|
||||
// cupcakes-or-sugar-cookies, with an empty "Sugar cookies" line beneath.
|
||||
CateringMenu.PackageView parties = catering.menu().packages().get(1);
|
||||
assertThat(parties.rows()).extracting(CateringMenu.RowView::label)
|
||||
.containsExactly("Cake", "Cupcakes or sugar cookies");
|
||||
assertThat(parties.rows().get(0).values())
|
||||
.containsExactly("A 6-inch cake", "An 8-inch cake", "A 10-inch cake");
|
||||
assertThat(parties.rows())
|
||||
.noneMatch(row -> row.values().stream().allMatch(String::isEmpty));
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyLineCarriesOneEntryPerColumn() {
|
||||
// The invariant the whole aggregate exists to hold: if these ever fall out of step, a box is
|
||||
@@ -166,7 +185,7 @@ class CateringMenuTest {
|
||||
CateringMenu.PackageView saved = catering.everything().packages().get(0);
|
||||
assertThat(saved.tiers()).extracting(CateringMenu.TierView::label).containsExactly("Small", "Large");
|
||||
assertThat(saved.tiers()).extracting(CateringMenu.TierView::id).doesNotContain(medium);
|
||||
assertThat(saved.rows().get(0).values()).containsExactly("12 items", "24 items");
|
||||
assertThat(saved.rows().get(0).values()).containsExactly("A dozen", "Two dozen");
|
||||
assertThat(saved.rows()).allSatisfy(row -> assertThat(row.values()).hasSize(2));
|
||||
}
|
||||
|
||||
@@ -192,7 +211,7 @@ class CateringMenuTest {
|
||||
Long muffins = office.rows().get(0).id();
|
||||
|
||||
List<CateringMenu.RowEdit> rows = new ArrayList<>(asLines(office));
|
||||
rows.set(0, new CateringMenu.RowEdit(muffins, "Mini muffins", List.of("12 items", "20 items", "24 items")));
|
||||
rows.set(0, new CateringMenu.RowEdit(muffins, "Mini muffins", List.of("A dozen", "Twenty", "Two dozen")));
|
||||
catering.save(office.id(), new CateringMenu.PackageEdit(
|
||||
"Office boxes", "For meetings and staff mornings.", asEdits(office), rows, office.notes()));
|
||||
|
||||
@@ -201,7 +220,7 @@ class CateringMenuTest {
|
||||
assertThat(saved.blurb()).isEqualTo("For meetings and staff mornings.");
|
||||
// Same line, edited — not a new line that happens to read the same.
|
||||
assertThat(saved.rows().get(0).id()).isEqualTo(muffins);
|
||||
assertThat(saved.rows().get(0).values()).containsExactly("12 items", "20 items", "24 items");
|
||||
assertThat(saved.rows().get(0).values()).containsExactly("A dozen", "Twenty", "Two dozen");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -209,7 +228,7 @@ class CateringMenuTest {
|
||||
CateringMenu.PackageView fresh = catering.add("Holiday boxes");
|
||||
|
||||
assertThat(catering.everything().packages()).extracting(CateringMenu.PackageView::name)
|
||||
.containsExactly("Office", "Parties", "Weddings", "Holiday boxes");
|
||||
.containsExactly("Office boxes", "Parties", "Weddings", "Holiday boxes");
|
||||
assertThat(catering.menu().packages()).extracting(CateringMenu.PackageView::name)
|
||||
.doesNotContain("Holiday boxes");
|
||||
|
||||
@@ -233,7 +252,7 @@ class CateringMenuTest {
|
||||
entityManager.clear();
|
||||
|
||||
assertThat(catering.everything().packages()).extracting(CateringMenu.PackageView::name)
|
||||
.containsExactly("Office", "Parties");
|
||||
.containsExactly("Office boxes", "Parties");
|
||||
// The page's own terms outlive any one table.
|
||||
assertThat(catering.menu().notes()).hasSize(2);
|
||||
}
|
||||
@@ -244,9 +263,9 @@ class CateringMenuTest {
|
||||
List<Long> weddingsFirst = List.of(tables.get(2).id(), tables.get(0).id(), tables.get(1).id());
|
||||
|
||||
assertThat(catering.reorder(weddingsFirst)).extracting(CateringMenu.PackageView::name)
|
||||
.containsExactly("Weddings", "Office", "Parties");
|
||||
.containsExactly("Weddings", "Office boxes", "Parties");
|
||||
assertThat(catering.menu().packages()).extracting(CateringMenu.PackageView::name)
|
||||
.containsExactly("Weddings", "Office", "Parties");
|
||||
.containsExactly("Weddings", "Office boxes", "Parties");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -96,16 +96,21 @@ class SiteControllerTest {
|
||||
@Test
|
||||
void theCateringTablesAreRenderedFromTheDatabase() throws Exception {
|
||||
mvc.perform(get("/catering")).andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("Office")))
|
||||
.andExpect(content().string(containsString("Office boxes")))
|
||||
.andExpect(content().string(containsString("Weddings")))
|
||||
// Prices as the server writes them — the page never formats money.
|
||||
.andExpect(content().string(containsString("$24")))
|
||||
.andExpect(content().string(containsString("$236")))
|
||||
// A cell, and a note.
|
||||
.andExpect(content().string(containsString("6+6+6 or 12+6")))
|
||||
.andExpect(content().string(containsString("Minimum of 6 items per baked good")))
|
||||
// Both renderings of the same table are present; CSS decides which one is visible.
|
||||
.andExpect(content().string(containsString("<table")));
|
||||
// A cell and a note, in the wording a customer reads rather than the spreadsheet's.
|
||||
.andExpect(content().string(containsString("Eighteen, in two or three flavors")))
|
||||
.andExpect(content().string(containsString("baked in sixes")))
|
||||
// One card per size, and one enquiry link per table — not one per card, which would have
|
||||
// read "Ask about the 15–20 people".
|
||||
// The size label is in the HTML as written; the small caps are CSS.
|
||||
.andExpect(content().string(containsString("Large")))
|
||||
.andExpect(content().string(containsString("Ask about office boxes")))
|
||||
// The price grid is gone: it was the source spreadsheet, rendered.
|
||||
.andExpect(content().string(not(containsString("<table"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user