fix(engagement): the six defects the browser pass found (Phase 4b)
Driving the two screens in Chrome, after the API walk had already found the two
in Phase 4a's code. None of these is visible from a test or from curl.
Two cost an operator something real:
- The Audience dropdown rendered EMPTY before a trigger was chosen. There is
genuinely nothing it may offer without a ceiling, but a select with zero
options reads as broken rather than as waiting. It now says "Choose a
trigger first..." and is disabled.
- A `members` audience with no saved audience reaches NOBODY, and only the
preview button said so. That is the design, but it is also the default the
instant a members-ceiling trigger is picked - so the rule saves, gets
switched on, and mails nobody with nothing on screen saying so. The editor
now says it inline, and stands down once a preview has answered the same
question more precisely.
One the server was already refusing, just too late:
- The composer offered "exclude" on the only row, building an `and` whose
every child is a complement. The server refuses it correctly but only after
a save, and it is one checkbox away at all times. Now refused inline, in the
operator's words.
Three wording and layout:
- the template-key input truncated its placeholder, and said "optional until
Phase 5" - a sentence about the plan document, not about the deployment
- "segment" leaked into a screen that says "saved audience" everywhere else.
The API, schema and docs keep saying segment (one word for one table);
translated at the point of display only
- the composer repeated its AUDIENCE heading above every row
Client only - no server change, so swagger and the route manifest are untouched.
Client suite 316/316; all six verified in the browser after the fix.
- [x] AI-assisted: written with Claude Code (Opus)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
||||
describeRule,
|
||||
describeExpression,
|
||||
notPlacementError,
|
||||
audienceWarning,
|
||||
operatorWords,
|
||||
conditionRowsFrom,
|
||||
conditionsFromRows,
|
||||
operatorsForType,
|
||||
@@ -218,6 +220,34 @@ test('a single stored comparison is one editable row', () => {
|
||||
|
||||
// ── Segment composition ────────────────────────────────────────────────────
|
||||
|
||||
test('a members audience with no saved audience is warned about BEFORE the save', () => {
|
||||
// The trap the browser walk found: it is the default the moment a
|
||||
// members-ceiling trigger is chosen, and the rule it produces saves, switches
|
||||
// on and mails nobody. Nothing on the screen said so unless you pressed
|
||||
// Preview.
|
||||
assert.match(audienceWarning({ audience: 'members', audienceSegmentId: null }), /reaches nobody/)
|
||||
assert.equal(audienceWarning({ audience: 'members', audienceSegmentId: 4 }), null)
|
||||
assert.equal(audienceWarning({ audience: 'owner', audienceSegmentId: null }), null)
|
||||
})
|
||||
|
||||
test('the server says "segment"; the screens say "saved audience"', () => {
|
||||
// One word for one table in the API, the schema and the docs. But an operator
|
||||
// meets the concept under a heading that says "Audiences", and a sentence that
|
||||
// switches vocabulary mid-screen reads as being about something else.
|
||||
assert.equal(operatorWords('audience segment is dormant'), 'audience saved audience is dormant')
|
||||
assert.match(describeReach({ count: 0, dormant: true, reason: 'audience segment is dormant' }), /saved audience/)
|
||||
// and it does not maul a word that merely contains it
|
||||
assert.equal(operatorWords('segmented data'), 'segmented data')
|
||||
})
|
||||
|
||||
test('a list of nothing but exclusions is refused before the round trip', () => {
|
||||
// One checkbox away at all times, because the composer offers "exclude" on
|
||||
// every row including the only one. The server refuses it correctly — but
|
||||
// only after a save.
|
||||
const err = notPlacementError({ op: 'and', nodes: [{ op: 'not', nodes: [{ audienceId: 'a' }] }] })
|
||||
assert.match(err, /at least one audience/i)
|
||||
})
|
||||
|
||||
test('a bare not is refused before it reaches the server', () => {
|
||||
assert.ok(notPlacementError({ op: 'not', nodes: [{ audienceId: 'uo.governors' }] }))
|
||||
assert.ok(notPlacementError({ op: 'or', nodes: [{ audienceId: 'a' }, { op: 'not', nodes: [{ audienceId: 'b' }] }] }))
|
||||
|
||||
Reference in New Issue
Block a user