| 1 | # 🤖 The bakeoff contract |
| 2 | |
| 3 | Every app in the bakeoff answers the same requests the same way, so one |
| 4 | test suite and one measurement harness can drive all of them. This file |
| 5 | is binding. If something here seems wrong, say so in your `STATUS.md` |
| 6 | rather than deviating. |
| 7 | |
| 8 | ## Pages |
| 9 | |
| 10 | | Request | Response | |
| 11 | |---|---| |
| 12 | | `GET /` | The whole list | |
| 13 | | `GET /active` | Only items not completed | |
| 14 | | `GET /completed` | Only completed items | |
| 15 | | `GET /<filter>?edit=<id>` | The same page, with that item's `li` given the class `editing` and its `input.edit` inside a form that posts to `/todos/<id>/edit` | |
| 16 | | `GET /static/base.css`, `GET /static/index.css` | `shared/base.css` and `shared/index.css`, byte for byte | |
| 17 | | `GET /healthz` | 200 | |
| 18 | | `GET /api/version` | 200, the body is the build version | |
| 19 | |
| 20 | The filter links are `href="/"`, `href="/active"` and `href="/completed"`, |
| 21 | and the current one has the class `selected`. `#/` routes aren't used. |
| 22 | |
| 23 | ## Actions |
| 24 | |
| 25 | Every action is an HTML form POST (`application/x-www-form-urlencoded`). |
| 26 | Without any special request headers, each one answers **303 See Other** |
| 27 | back to the current filter. |
| 28 | |
| 29 | | Request | Fields | Effect | |
| 30 | |---|---|---| |
| 31 | | `POST /todos` | `title`, `filter` | Adds an item. The title is trimmed; if nothing is left, nothing is added | |
| 32 | | `POST /todos/<id>/toggle` | `filter` | Flips that item between active and completed | |
| 33 | | `POST /todos/<id>/edit` | `title`, `filter` | Renames it. The title is trimmed; if nothing is left, the item is deleted | |
| 34 | | `POST /todos/<id>/destroy` | `filter` | Deletes it | |
| 35 | | `POST /todos/toggle-all` | `filter` | Marks every item completed. If every item already was, marks them all active | |
| 36 | | `POST /todos/clear-completed` | `filter` | Deletes every completed item | |
| 37 | |
| 38 | - `filter` is `all`, `active` or `completed`. The 303 goes to `/`, |
| 39 | `/active` or `/completed` to match. A missing or unknown value goes |
| 40 | to `/`. |
| 41 | - A POST naming an `<id>` that isn't in the caller's own list answers |
| 42 | **404**. Nothing changes, in that list or anyone else's. |
| 43 | - A POST with an `Origin` header that isn't this app's own origin |
| 44 | answers **403**. The app's own origin is `https://$CARLOS_HOST` on |
| 45 | Carloku, and `http://<listen address>` when run with `--listen`. A |
| 46 | POST with no `Origin` header is allowed. There's no CSRF token. |
| 47 | - Your JavaScript may call these same endpoints with a request header of |
| 48 | your choosing (for example `Accept: application/json` or a custom |
| 49 | header) and get HTML fragments or JSON back. Without that header, the |
| 50 | behaviour above must not change. |
| 51 | |
| 52 | ## Markup |
| 53 | |
| 54 | Start from `shared/reference.html`, which is TodoMVC's own template. |
| 55 | With JavaScript on, your page must look like it. The suite compares |
| 56 | screenshots, masking only `footer.info`. |
| 57 | |
| 58 | - Every `li` in `ul.todo-list` carries `data-id="<id>"`, has the class |
| 59 | `completed` when completed, and has `editing` while being edited. |
| 60 | - `section.main` and `footer.footer` are hidden when the list is empty |
| 61 | (all filters considered). `button.clear-completed` is hidden when |
| 62 | nothing is completed. |
| 63 | - The counter reads `<strong>N</strong> item left` or |
| 64 | `<strong>N</strong> items left`. |
| 65 | - The footer line "Created by …" reads |
| 66 | `Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · <LANGUAGE>`, |
| 67 | where `<LANGUAGE>` is your setup's name, e.g. `Rust` or |
| 68 | `Go + Rastrillo`. |
| 69 | |
| 70 | ### Controls that must work without JavaScript |
| 71 | |
| 72 | The suite drives a page with JavaScript off through exactly these: |
| 73 | |
| 74 | | Action | Control | |
| 75 | |---|---| |
| 76 | | Add | `input.new-todo` in a form posting to `/todos`. Pressing Enter submits it | |
| 77 | | Toggle one | A submit control matching `[data-action="toggle"]` inside that item's `li`, in a form posting to `/todos/<id>/toggle` | |
| 78 | | Edit | A link `a[data-action="edit"]` inside that item's `li`, going to `?edit=<id>` on the current filter. The edit view's `input.edit` submits on Enter | |
| 79 | | Delete | `button.destroy`, a submit button in a form posting to `/todos/<id>/destroy` | |
| 80 | | Toggle all | A submit control matching `[data-action="toggle-all"]`, in a form posting to `/todos/toggle-all` | |
| 81 | | Clear completed | `button.clear-completed`, a submit button in a form posting to `/todos/clear-completed` | |
| 82 | |
| 83 | These extra controls may be visible when JavaScript is off, styled to |
| 84 | fit. When JavaScript is on, the page must match the reference, so hide |
| 85 | or replace them. |
| 86 | |
| 87 | ### With JavaScript on |
| 88 | |
| 89 | The page behaves like TodoMVC's own apps. Double-click a label to edit. |
| 90 | Enter or blur saves, and Escape cancels. An empty title deletes. Clicking |
| 91 | `input.toggle` or `input.toggle-all` takes effect without a full page |
| 92 | load, and the counter, filters, "Clear completed" button and toggle-all |
| 93 | state stay correct. `input.new-todo` is cleared after adding. |
| 94 | |
| 95 | ## Lists |
| 96 | |
| 97 | - The whole app is one instance. Each visitor gets their own list through |
| 98 | a cookie set on their first visit. The cookie holds a random value with |
| 99 | at least 128 bits of randomness and is `HttpOnly; SameSite=Lax; |
| 100 | Path=/`, plus `Secure` when served over https. |
| 101 | - A cookie value the app doesn't recognise, garbled or not, gets a fresh |
| 102 | empty list, never an error. |
| 103 | - Titles are shown as text, never as HTML. Any Unicode is kept exactly as |
| 104 | typed, and titles up to at least 10,000 characters work. |
| 105 | - `Content-Length`, when sent, is the number of bytes in the body. |
| 106 | |