The fields of a case
Beside its title and its body, a case carries four things about how it matters and one about
what it is tagged. All of them travel through POST /v1/test-cases and PATCH /v1/test-cases/{id},
and every one of them appears in the audit log as “was → became”.
Description and preconditions
Section titled “Description and preconditions”Two free-text fields, description and precondition, up to 16 KiB each. They are shown as written —
line breaks kept, no markup rendered yet — and on the dashboard they sit under their own headings on
the case page and as two text areas on the form. Sending null in a PATCH clears one; leaving it
out keeps it.
These two are the text of the case, so changing either raises the case’s version — a result
that points at version 3 tested what version 3 said. Priority, tags, position and the suite are
metadata: changing them does not move the version.
The steps of a manual case are text too, and each action is also a row of the project’s Steps Database: the form suggests the steps the project already has as you type, and renaming a step there rewrites it in every case that carries it.
Parameters
Section titled “Parameters”A case that should run on several sets of data carries one table — the way Testomat has its
dynamic parameters: params are the column names, every row of rows is one iteration, with
exactly one value per column.
{ "title": "login as ${Username} @smoke", "steps": [{ "action": "Sign in as ${Username}", "expected": "The ${Role} home opens" }], "examples": { "params": ["Username", "Role"], "rows": [["admin", "admin"], ["user", "user"]] }}${Name} — or {{Name}} — in the steps, description, preconditions and prompt stands for that
column’s value; in the title only ${Name}. Names are case-sensitive (Username is not
username) and unique; 1 to 16 of them, up to 64 characters, without $, { or }; 1 to 100
rows; the whole table under 16 KiB. A row of the wrong length, a duplicate name, a table without a
header row — each is refused with its rule:
400 · validation failed — examples.params: parameter names must be uniquenull in a PATCH clears the table; leaving it out keeps it. The table is content, like the
two texts: changing it raises the case’s version. Every row is run and reported on its own — a
result carries the row it ran as params, and the case’s history shows them, so a red line says
which set of data failed. In the Markdown document the table is the
<!-- example --> block at the end of the case.
On the dashboard: a Parameters section in the form on every type of case — the table as
fields, + column / + row, ✕ on each; and on the case page the table with a row per iteration
and, when the title has a ${Name}, the title each row runs under.
Priority
Section titled “Priority”Five levels, in rank order:
low · normal · important · high · criticalnormal is the default. The order is not decoration — priorityMin=high on the
list means “high and critical” because the levels are ranked, and the
dashboard shows each as a distinct glyph beside the title so the scale reads without colour.
Anything outside the five is refused with the list:
400 · validation failed — priority: must be one of low, normal, important, high, criticalAssignee
Section titled “Assignee”Who is to run the case: assigneeId, the userId of a member of the project, or absent while
nobody has been asked. It is set after the fact, never on create:
PATCH /v1/test-cases/{id}{ "assigneeId": "6f1c2b3a-0000-4000-8000-0000000000a2" }null takes the case back; a PATCH that does not name the field keeps whoever holds it. Anyone
who is not in the project is refused with the reason — a real user in another project the same way
as an id nobody holds, because the surface says nothing about who exists elsewhere:
400 · user '…' is not a member of this projectAssigning is metadata, like a priority: the case’s version does not move. Leaving the project, or
being removed from it, takes the person off every case they held there — «the assignee is a member»
stays true, rather than true at the moment of writing.
On the dashboard the case page shows the assignee as a circle with a name and a select behind it, «Unassigned» on top; a row of the tree carries the circle; the filter bar’s Assignee facet is Me · Unassigned · the members; and the selection bar’s Assign… hands out (or takes back) several at once — Many cases at once. A launch with Prefer case assignee copies the assignees into the run — Manual runs.
Comments
Section titled “Comments”What people said about the case, beside the case — «step 3 is stale since the checkout redesign» belongs where the next person to run it will read it. Plain text with its line breaks, no Markdown; one author, one moment, no edit — a changed mind is a second comment.
GET /v1/test-cases/{id}/comments → { "comments": [{ "id", "authorId", "body", "createdAt" }] }POST /v1/test-cases/{id}/comments { "body": "Step 3 is stale since the checkout redesign." }DELETE /v1/comments/{id}The list is the live comments, oldest first; the author is a userId, named on the dashboard from
the members list. The body is trimmed, never empty and at most 8 192
bytes — the same cap the column enforces, so it is a 400 with the reason rather than a 500.
Anyone in the project reads them, a reader too; a reader who tries to write is refused at the
boundary, 403 read-only member.
Deleting is soft — the row stays for the audit entry that names it, the list stops showing it — and
it is the author’s to do, or an owner’s on anyone’s. Somebody else’s comment answers 404 to
everybody else, the same answer an id nobody has gets, on purpose. A case in the bin, or one in
another project, has no comments to show and takes none: the case’s own 404.
Both writes land on the case’s audit feed as test_case/comment, the text in the diff — written
or taken back. They are not undone from the feed: a comment is deleted on the case.
On the dashboard the case page ends with Comments · N: a row is the person, the time and the text; Delete on your own (an owner: on anyone’s); the box and Comment below the list for a member, none for a reader. A comment posted appears in place, a deleted one leaves in place — no reload either way.
Position
Section titled “Position”position is a case’s place among the cases of its suite (or among the unfiled ones), dense and
zero-based. The list comes back in that order, suite by suite; sending a new position in a PATCH
moves the case and renumbers its neighbours. Suites have the same field for the same reason.
Tags live in the title
Section titled “Tags live in the title”A tag is written in the title, as @name, and that is where it is stored:
Checkout keeps the total right @smoke @paymentsThe rules are the ones the wider tool ecosystem already uses: a tag starts the title or follows a
space or a tab, its body is made of \w = - _ ( ) . : & and ends in a word character or ), it is
shorter than 120 characters, and its case is yours — Smoke and smoke are two tags. An e-mail
address or a @ in the middle of a word is not a tag.
The API reads them out for you: every case answers with tags — the @names of its title, without
the @, each once, in order — and the dashboard shows the clean title with the tags as chips.
You can write them back either way:
- put them in the
title—"title": "Checkout keeps the total right @smoke"; - or send
tagsbeside a clean title —"title": "Checkout keeps the total right", "tags": ["smoke"], and the stored title gets the@smoketail written for you.
When both are sent, tags wins over the title’s own tail; "tags": [] strips every tag. Two refusals
guard the field:
| Refusal | When |
|---|---|
400 · validation failed — title: a title cannot consist of tags only | nothing is left once the tags are removed |
400 · validation failed — tags.1: invalid tag 'smoke test' — a tag is 1 to 119 characters from \w = - _ ( ) . : & | a tag breaks the rule — the refusal names it |
Renaming and merging tags
Section titled “Renaming and merging tags”GET /v1/settings/tags lists the project’s tags with how many cases carry each — Smoke beside
smoke, each with its own count. The same list is Settings → Tags on the dashboard, and two
operations act on it:
- Rename —
POST /v1/settings/tags/renamewith{ "from": "smok", "to": "smoke" }rewrites the title of every case that carriesfrom, one audit entry per case. Iftoalready exists the two simply merge;tois checked against the tag rule first. - Merge —
POST /v1/settings/tags/mergewith{ "from": ["Smoke", "smoke-test"], "into": "smoke" }replaces every tag infromwithinto, in every title, in one transaction;intoneed not exist yet, andfrommust not contain it.
Both answer how many cases they touched, and neither moves a case’s version — a tag is metadata.