-
-
Notifications
You must be signed in to change notification settings - Fork 29
Developer Tests Forms
Part of Developer Tests. Seven suites covering the Forms module: how a question keeps its identity when a form is edited, how conditions are evaluated in two languages at once, how a form is laid out, and the three things a lookup field made newly possible.
| Test | Needs |
|---|---|
forms-logic/run.php |
Database, optionally headless Chrome |
form-layout.php |
Nothing |
field-widths-agree.php |
Nothing |
form-class-collisions.php |
Nothing |
form-drafts.php |
Database |
form-audiences.php |
Database |
forms-lookup/run.php |
Database (read-only) |
Two quite different things.
Field identity. Editing a form used to sync its fields by position. Drag a question and the labels were rewritten while the stored answers stayed where they were β so every historic submission silently began reading against the wrong question, and removing a field hard-deleted the last one's answers. Fields are now identified by id.
The condition evaluator. Conditional visibility exists twice:
includes/form_logic.php decides on submit, assets/js/form-logic.js shows and
hides as someone types. Two copies that disagree is the entire risk.
For identity, it does the thing you cannot do by reading code: records a real submission, edits the form the way the builder does, then reads the answers back and checks they are still attached to the questions they were given to.
For the evaluator, it runs the same table of cases through both copies and compares the answers. The JS half is driven through headless Chrome.
It does not use the single-transaction trick other suites use β FormsService
opens its own transactions and MySQL has no nested ones. Instead it creates
throwaway forms prefixed ZZ_TEST_ and removes them in a finally block.
php tests/forms-logic/run.php
Ends with a pass/fail count. Watch for SKIPPED: without headless Chrome the
entire JavaScript half is skipped, and it says so rather than quietly passing.
A green run with that section skipped has proved only half of what you wanted β
it has not compared the two evaluators at all.
-
An identity assertion β something in
FormsService::syncFields()has gone back to matching on position or order. This is the serious one: it corrupts historic submissions silently, so treat a red here as blocking. -
An evaluator mismatch β the two copies have drifted. The failure names the
case. Fix whichever side is wrong, and remember the change has to land in
both
includes/form_logic.phpandassets/js/form-logic.js.
A form's layout as an object separate from its questions, so a form can be re-laid-out without touching a question. Two things have to hold and neither would fail loudly:
- A form with no stored layout must derive exactly what it already draws.
Every form predating the feature stores
NULL, so a wrong derivation would silently re-lay-out every existing form on every install. - A stored layout must never drop a question. The case that matters is a field added or retired after the layout was saved β a field the layout does not mention would simply never render.
Pure logic, no database. It builds synthetic field lists with a small fld()
helper, derives layouts from them and reconciles stored layouts against changed
field lists, asserting on the result.
php tests/form-layout.php
43 assertions.
All ok, ending 43 passed, 0 failed.
A derivation failure means an existing form would be re-laid-out on upgrade β
compare what the derivation produced against what the pool order says it should.
A reconciliation failure means a question can vanish from a form; look at how
FormsService merges stored cells with the live field pool, particularly for
fields that are new or retired.
Field widths are expressed in twelfths, and the mechanism has two halves that must both be right.
The list of permitted widths is written down in more than one place β
FormsService::FIELD_WIDTHS (what may be saved) and WIDTHS in
assets/js/form-logic.js (what the fillers render) β and they must agree. The
builder must defer to FormLogic rather than declare a third list.
The grid container, which is what makes a width mean anything. A field can
carry data-width="6" all it likes; unless its parent is the 12-column grid, it
spans the row. So for each of the three surfaces that draw a form β the analyst
filler, the portal and the builder preview β the test checks the container class
is really emitted, a stylesheet really defines it as display: grid, the page
really loads that stylesheet, and it has a rule for every width the service
will accept.
Source reading and pattern matching. It also asserts structural rules about the list itself: the default must be on it and must be the full row (so absent means unchanged), and every width must have a partner that completes the row β 9 with 3, 8 with 4, 6 with 6. Note that is not the same as "12 is divisible by the width": 9 and 8 are on the list precisely because asymmetric pairs are what a real document wants.
php tests/field-widths-agree.php
44 assertions.
All ok, ending 44 passed, 0 failed. The two CONTROL lines at the end prove
the comparison itself works.
-
"form-logic agrees with the service" β you added or removed a width in one
place. The detail line prints both lists side by side. Neither half errors on
its own: a width in the builder but not the service is offered and then refused
on save; a width in the service but not
form-logicsaves and renders as full. - "β¦has a rule for width N" β you added a width to the list but not to one of the three grid stylesheets. It will render full width on that one surface only.
-
"emits the container class" β a container class was renamed in the markup
but not the stylesheet, or vice versa. This is the check that exists because
the portal shipped with
class="cat-form-table", a class defined in no stylesheet at all, while its real grid sat unreferenced inself-service.css.
That the Forms module's CSS class names do not collide with rules in the stylesheets every page already loads.
It exists because a table question was given class="form-grid", and inbox.css
had long defined .form-grid { display: grid; grid-template-columns: 1fr 1fr; }.
Every Forms page loads inbox.css, so the <table> became a two-column grid and
its <thead> and <tbody> were laid out side by side.
inbox.css. A harness
missing a stylesheet the real page loads is not a rendering environment, and it
will report a broken screen as perfect.
It parses the class names each stylesheet defines, and the class names the
Forms pages emit, and looks for overlap. form-grid is additionally checked by
name so it cannot come back quietly, with a control asserting the parser
really does see .form-grid in inbox.css β if that control fails, every "no
collision" result above it is worthless.
Class attributes are split on whitespace and compared as whole tokens. An
earlier version matched with \b, which treats a hyphen as a word boundary, so
the innocent class="cat-form-grid" was reported as emitting form-grid.
php tests/form-class-collisions.php
11 assertions.
A genuine collision means a rule from another module is styling Forms markup that never asked for it. Rename the Forms class β it is the newcomer. Do not rename the other one; something else depends on it.
Drafts β a form somebody started and did not finish. Four things, none of which fails loudly on its own:
- A draft is never validated. Not being finished is the point: a required field may be empty and a number box may hold "tbc".
- A draft cannot park arbitrary data. Keys must be field ids belonging to that form; anything else is dropped rather than stored.
-
A draft is pinned to the form version it was typed into, and one whose
version has been superseded is reported as stale.
createVersion()renumbers every field, so loading old answers into a newer version would attach them to whatever now holds those ids β silently, and wrongly. -
Drafts stay out of
form_submissions. They live in their own table, so nothing that reads submissions β the list, collections, counts, exports, the approval inbox, workflow triggers, REST β had to be taught anything.
Runs against the real database, creating a form and drafts against it, then
versioning the form to prove the stale path. Everything is deleted in a finally
block and the cleanup is asserted, not assumed.
php tests/form-drafts.php
If it prints a line about draftsAvailable and stops, the form_drafts table
does not exist β run System β Database Verification and try again. That is a
skip, not a pass.
- Validation crept in β something now rejects an incomplete draft. That breaks the only thing the feature is for.
- The stale check β a draft is being loaded into a version it was not typed into. Answers will land in the wrong boxes.
-
Cleanup assertions β rows were left behind; look at the
finally.
Restricting a form to a group of people. A restriction is only worth the name if every way in enforces it β the catalogue hiding a card is not a check, since someone who was in the group last week, or who has a colleague's link, arrives at the other endpoints with a perfectly valid form id. So it checks all five:
- the catalogue list β the form is not in it
- opening it by id β 404, the same as a form that does not exist
- saving a draft of it β refused
- fetching its images β 404
- submitting it β refused in the service, not just the adapter
π No rows means everyone, which is every form that predates the feature. The first thing the test proves is that an unrestricted form is unaffected β a restriction feature that quietly narrows existing forms is the worst possible outcome.
php tests/form-audiences.php
Needs the database. Everything it creates is removed in a finally block and the
cleanup is asserted.
A message about form_audiences not existing means the table is missing β run
System β Database Verification. That is a skip.
The label names the door that let someone through. Fix it at the service layer, not in the adapter that happened to be tested β the point of the test is that there are five doors and they must all be locked by the same lock.
Lookup fields, which answer "which one?" by searching records the app already holds. That makes them the first field type whose possible answers are built at answer time out of live, company-scoped data, so three things can go wrong that no earlier field type could:
- Scoping. The search must never return a record the person asking may not see. A form is a place where a customer types, so "the widget only shows their own kit" has to be true on the server, not in the UI.
- Tampering. The posted answer is an id, and nothing stops a crafted request naming someone else's asset. If it were accepted, the submission would render that asset's name to an analyst who would reasonably believe it. This is the generalisation of the older rule that a dropdown must be answered with one of its own choices.
- Drift. A field type has to be added in several places at once β the service whitelist, the AI generator's whitelist and its prompt, the shared JS evaluator, the builder. The prompt is prose, so nothing fails when it falls behind; it just quietly stops offering the type, or offers one that no longer exists. The suite reads all five and compares them.
Read-only β it searches existing records and never writes. Every "it refused" assertion is paired with a positive control that the same call accepts something legitimate, because a function that refuses everything (a wrong column name, say) would otherwise look like a clean pass.
php tests/forms-lookup/run.php
- A scoping failure is a live data leak. Treat it as blocking.
- A control failure means the opposite: lookups are refusing everything and the feature is broken, even though the "it refused" assertions look green.
- A drift failure names the two sources that disagree. The AI prompt is the one that falls behind silently β check it first.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96