Skip to content

Commit 84ce59c

Browse files
feat(core): show a holeless confirmText / successMessage as written (objectui#13476) (#13482)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DYL5YEa1zRjoXpJH1JbeMbZ)_ Fixes #13476 Clause-②: no Metadata-protocol: narrowed ## Summary objectui#13367 kept one transitional reading in `ActionRunner`: a `confirmText` or `successMessage` sentence with no `{{ }}` hole still had its `${…}` tokens filled. This PR removes both branches, so a holeless sentence is shown exactly as written, the same as any other copy with no hole. It is the objectui half of #25702; #25709 (warning to error, delete `ACTION_COPY_DOLLAR_TRANSITION_ROOTS`) waits for objectstack's `.objectui-sha` to carry this merge. ## Changes - `confirmMessage`: the `!hasTemplateHoles(text)` branch returns `text` instead of `templates.evaluate(text)`. The method no longer takes the evaluator; the param dialog's `interpolate` (still the `${…}` evaluator, out of scope) now builds it right where it is used. - `composeSuccessMessage`: a holeless `successMessage` returns `successCopy` instead of running `substituteScopeTokens`. - Docblocks updated: `ActionDef.confirmText`, `ActionDef.successMessage`, the `RUNNER_TEXT` note, `composeSuccessMessage`, `confirmMessage`, `templateEvaluatorFor`, `interpolateTarget`'s note and `substituteScopeTokens` (the grammar's one toast caller is now `outcomeMessages`), plus `hasTemplateHoles` in `actionCopyTemplate.ts`. - Docs: `content/docs/core/enhanced-actions.mdx` "Post-success behaviour" now says `outcomeMessages` reads `${result.*}` and `successMessage` reads `{{ result.* }}` / `{{ record.* }}`; the complex-action example's `confirmText` uses a `{{ record.customer_name }}` hole. - Changeset `.changeset/13476-holeless-action-copy.md`: `@object-ui/core` minor (a narrowing; objectui marks breaking changes `minor`). **Untouched, per the card:** `outcomeMessages` keeps its `${result.*}` reading through `substituteScopeTokens`; `errorMessage` is still shown as written. The claim's file surface was amended by the seat (card comment 6391456644) for `actionCopyTemplate.ts`, the docs page, and the app-shell test. Two more test files outside it also drove the old reading end to end and are converted here: `packages/plugin-grid/src/__tests__/rowActionRecordHole-12967.test.tsx` and `packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx`, plus `packages/react/src/hooks/__tests__/useActionTextLocalizer.outcomeMessages-11344.test.tsx`. ## Tests Every fixture that fed the runner a holeless `${…}` `confirmText` / `successMessage` now authors the `{{ }}` hole the runner reads, and three rows pin the new behaviour: - `ActionRunner.copyTemplate-13367`: "a `${…}` token in a sentence with no hole is text, not an expression" (header door and row door), and "a `${result.*}` token in a sentence with no hole is text, not filled". - `ActionRunner.outcomeMessages-11344`: "rung 2 — a `${result.*}` token in a successMessage with no hole is text". The "reads the `result` scope only" row moves to an `outcomeMessages` entry, the one toast rung that still uses the `${…}` grammar. - `ActionRunner.rowRecordTemplates-12967`: the row-less control now reads `Pause ?` (an unbound `{{ record.* }}` root prints empty) where the evaluator left `${record.display_name}` raw. Runs, at head `55dfcfd`, from the repo root: - `pnpm exec vitest run packages/core/src/actions/`: 41 files, 625 tests passed. - Converted and neighbouring suites (app-shell invite feedback, toolbar script selection, `onSuccess.navigate` hosts, param-dialog record holes, `consoleServerAction`, components `renderers/action/`, element-button i18n copy, plugin-grid row-action record hole, console registry parity, react `hooks/`, app-shell `hooks/__tests__/`, record-alert, ListView). The first sweep (44 files, 968 tests) failed 3 rows in `rowActionRecordHole-12967` and `toolbarScriptSelection-12331`, which still authored `${…}`; after converting them, those 2 files pass (11 tests). The second sweep: 120 files, 1233 tests passed. - `pnpm --filter @object-ui/core type-check`: exit 0, after building the core dependency closure. **Ablation** (commit first, then mutate, trap-restored): with `ActionRunner.ts` reverted to `origin/main`, the three new rows fail (`Tests 3 failed | 45 passed (48)`); restored, `git diff HEAD` is empty and the blob hash matches HEAD. ## Gates (local subset) - `check:new-line-citations`, `check:control-bytes`, `check:doc-fences`, `check:doc-types`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:action-forward-parity`: exit 0. - `node scripts/check-changeset-presence.mjs`: passes, 1 changeset declared. - NOT MEASURED locally: `check:doc-snippets` and `check:doc-examples` (exit 2, precondition: the doc-snippet build closure is not built here). CI runs both; the docs edit changes one string literal inside a typed snippet. - Full `pnpm test` and `pnpm lint` are CI's. ## Acceptance notes - No in-tree producer authors the holeless `${…}` shape: no non-test `confirmText` / `successMessage` in this repo carries `${`, and objectstack's measurement (card) found none in examples, `platform-objects`, `plugin-approvals` or HotCRM. - Fixtures left as `${…}` on purpose: `element-button-i18nCopy-12757` and `action-outcomeMessages-forward-11344` (components) only pin that copy reaches the runner unchanged, or never reach rung 2; the react localizer's `outcomeMessages` fixtures are rung 1, which keeps `${result.*}`. - `ActionParamDialog.tsx`'s `interpolate` docblock still says it fills `${...}` "in the template scope the runner reads `confirmText` in". The record binding is still shared; the dialect differs now. Carrier: none, noted only. - [x] Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer) --- _Generated by [Claude Code](https://claude.ai/code/session_01P5j5dvWsBeNZNp2qLwwRqH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 610d9ef commit 84ce59c

13 files changed

Lines changed: 129 additions & 93 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/core": minor
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: narrowed
7+
8+
An action's `confirmText` or `successMessage` with no `{{ }}` hole is now shown exactly as written (objectui#13476).
9+
10+
objectui#13367 kept one transitional reading: a sentence with no `{{ }}` hole still had its `${…}` tokens filled, `${record.*}` in the confirm dialog and `${result.*}` in the success toast. That reading is gone. A `${…}` token in either text is now plain text, the same as in any other copy with no hole. To put a value into the sentence, write a `{{ }}` hole: `Archive {{ record.name }}?`, `Invitation sent to {{ result.email }}`.
11+
12+
No in-tree producer writes the holeless `${…}` shape. objectstack's measurement of its examples, `platform-objects`, `plugin-approvals` and HotCRM found none. objectstack's lint already warns on the shape and refuses every other `${…}` token in this copy (objectstack#25702). The paired objectstack card turns the warning into an error once its objectui pin carries this change.
13+
14+
`outcomeMessages` entries are unchanged: they still fill `${result.*}`. `errorMessage` is unchanged: it is shown as written.

‎content/docs/core/enhanced-actions.mdx‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,15 @@ What to write instead:
323323
one message per snake_case `outcome` the handler returns in its answer. The
324324
success toast shows the `outcomeMessages` entry the answer's `outcome` names,
325325
else `successMessage` (objectui#11344). Both may read the handler's answer as
326-
`${result.*}` — `'Environment ${result.name} archived'` — the same scope
327-
`onSuccess.navigate` reads. A `message` in the server's answer is not shown
328-
unless the copy asks for it as `${result.message}`. Without either, the
329-
runner falls back to a generic "Action completed successfully".
326+
`result`. An `outcomeMessages` entry writes it as `${result.*}` —
327+
`'Environment ${result.name} archived'` — the same scope `onSuccess.navigate`
328+
reads. `successMessage` writes it as a `{{ }}` hole —
329+
`'Environment {{ result.name }} updated'` — and can also name the record the
330+
action ran against as `{{ record.* }}`; a `${…}` token in it is shown as
331+
written (objectui#13476). A `message` in the server's answer is not shown
332+
unless the copy asks for it (`{{ result.message }}` in `successMessage`).
333+
Without either, the runner falls back to a generic "Action completed
334+
successfully".
330335
That fallback is text the runner writes itself, and so are the "Action
331336
failed" error toast for an error with no readable message, a parallel
332337
chain's "One or more parallel actions failed", and the label it hands your
@@ -408,7 +413,7 @@ const complexAction: ActionSchema = {
408413

409414
// Confirm before processing
410415
actionType: 'confirm',
411-
confirmText: 'Process this order for ${data.customerName}?',
416+
confirmText: 'Process this order for {{ record.customer_name }}?',
412417

413418
// Execution gate - this action is the premium path; the standard path is a
414419
// sibling action carrying the opposite predicate

‎packages/app-shell/src/views/ObjectView.inviteFeedback-11817.test.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
* the dialog's Confirm — no objectui code drops it. Pinned with its
2525
* control (a refused invite raises the server's message and no success
2626
* toast), and with the copy that NAMES the invitee: the runner fills
27-
* `${result.*}` from the answer (objectui#11344), so an action whose
28-
* `successMessage` reads `${result.email}` gets the invitee's address.
27+
* `{{ result.* }}` from the answer (objectui#11344, objectui#13367), so
28+
* an action whose `successMessage` reads `{{ result.email }}` gets the
29+
* invitee's address.
2930
* Whether `invite_user` declares such copy is the action's metadata.
3031
* - **The subtitle.** An action that declares `description` gets it as the
3132
* dialog's subtitle; one that declares none gets the generic line. Both
@@ -286,7 +287,7 @@ describe('objectui#11817 — Invite User says it worked', () => {
286287
});
287288

288289
it('copy that reads the answer\'s `result.email` names the invitee', async () => {
289-
mountList('sys_user', 'all_users', [sysUser(`Invitation sent to \${result.email}`), ...MEMBERS]);
290+
mountList('sys_user', 'all_users', [sysUser('Invitation sent to {{ result.email }}'), ...MEMBERS]);
290291
await inviteAda();
291292

292293
expect(await screen.findByText('Invitation sent to ada@example.com', undefined, { timeout: 5000 })).toBeInTheDocument();

‎packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* record-scoped action refuses 0 and 2 rows with a toast and sends nothing; one
2828
* row sends one request, and its result reaches the user through the runner's
2929
* success toast — the action's own copy, filled from the answer's
30-
* `${result.*}`, or the runner's generic line when it declares none.
30+
* `{{ result.* }}`, or the runner's generic line when it declares none.
3131
*
3232
* The last block replays HotCRM's own declaration: a `list_item` placement and
3333
* a FIELD-BACKED param, so the dialog draws a record picker and the toast
@@ -249,7 +249,7 @@ describe('objectui#12331 — a record-scoped script action at list_toolbar, with
249249
});
250250

251251
it('1 row: exactly one request carries that row, and the result reaches the user', async () => {
252-
mountLeads(RECORD_SCOPED, `Added \${result.count}, skipped \${result.skipped}`);
252+
mountLeads(RECORD_SCOPED, 'Added {{ result.count }}, skipped {{ result.skipped }}');
253253
await tickRows(1);
254254
await launchFromHeader();
255255

‎packages/core/src/actions/ActionRunner.ts‎

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ export interface ActionDef {
158158
*
159159
* A string or the `template` envelope (objectui#13367, the renderer half of
160160
* objectstack#24814): its `{{ }}` holes are filled over `record` and
161-
* `params`, and a sentence with none keeps the transitional `${…}` reading.
162-
* See `confirmMessage`.
161+
* `params`, and a sentence with none is shown as written — a `${…}` token is
162+
* text (objectui#13476). See `confirmMessage`.
163163
*/
164164
confirmText?: ActionCopyTemplate;
165165
/**
@@ -276,8 +276,8 @@ export interface ActionDef {
276276
toast?: { showOnSuccess?: boolean; showOnError?: boolean; duration?: number };
277277
/** Success message (from UIActionSchema). The success toast's second rung,
278278
* after {@link outcomeMessages}. A string or the `template` envelope
279-
* (objectui#13367): `{{ }}` holes over `result` and `record`, or, in a
280-
* sentence with none, the transitional `${result.*}` tokens. */
279+
* (objectui#13367): `{{ }}` holes over `result` and `record`; a sentence
280+
* with none is shown as written, `${…}` tokens included (objectui#13476). */
281281
successMessage?: ActionCopyTemplate;
282282
/** Error message (from UIActionSchema). A string or the `template`
283283
* envelope (objectui#13367); shown as written — no scope is declared for
@@ -626,8 +626,8 @@ export type ToastHandler = (message: string, options?: {
626626
*
627627
* An author's `outcomeMessages` / `successMessage` / `errorMessage` and an
628628
* action's own error are never in this table: the host localizes authored copy
629-
* before dispatch, and the runner only fills in their holes (`{{ }}` or the
630-
* transitional `${result.*}`).
629+
* before dispatch, and the runner only fills in their holes (`{{ }}` in
630+
* `successMessage`, `${result.*}` in `outcomeMessages`).
631631
*/
632632
const RUNNER_TEXT = {
633633
completedSuccessfully: {
@@ -1521,12 +1521,11 @@ export class ActionRunner {
15211521
// (objectui#4314): it had zero producers and no bundle key, so a dialog
15221522
// authored that way silently lost localization.
15231523
//
1524-
// Read through `templateEvaluatorFor`, not the runner's own evaluator:
1525-
// a row dispatch binds no `record` in the runner's context, so a
1526-
// `${record.*}` hole the record header fills was shown raw from a list
1527-
// row (objectui#12967).
1528-
const templates = this.templateEvaluatorFor(action);
1529-
const confirmMessage = this.confirmMessage(action, templates);
1524+
// Its `{{ }}` holes read the dispatch's record (`dispatchRecord`), not
1525+
// the runner's context alone: a row dispatch binds no `record` there, so
1526+
// a hole the record header fills was shown raw from a list row
1527+
// (objectui#12967).
1528+
const confirmMessage = this.confirmMessage(action);
15301529
if (confirmMessage !== undefined) {
15311530
const confirmed = await this.confirmHandler(confirmMessage);
15321531
if (!confirmed) {
@@ -1589,6 +1588,10 @@ export class ActionRunner {
15891588
return lastRun;
15901589
};
15911590

1591+
// The dialog's `description` / `helpText` read the dispatch's record
1592+
// as `record`: a row dispatch binds none in the runner's context
1593+
// (objectui#12967).
1594+
const templates = this.templateEvaluatorFor(action);
15921595
const collected = await this.paramCollectionHandler(
15931596
paramDefs,
15941597
action,
@@ -1763,25 +1766,27 @@ export class ActionRunner {
17631766
*
17641767
* Rungs 1 and 2 are the author's copy. Both arrive already localized — the
17651768
* host's localizer resolves the bundle entry or the inline `I18nLabel` map
1766-
* before dispatch — and both have their `${result.*}` tokens filled in here,
1767-
* through the one token grammar and scope `onSuccess.navigate` reads
1768-
* (`substituteScopeTokens` over `readActionPayload(result.data)`). Only the
1769-
* sink differs: a toast is text, so a value is inserted as-is, where a URL
1770-
* position percent-encodes it.
1769+
* before dispatch — and both read the same payload,
1770+
* `readActionPayload(result.data)`, as `result`.
17711771
*
1772-
* Rung 2 is also a `template` slot (objectui#13367, the renderer half of
1772+
* Rung 1 has its `${result.*}` tokens filled in here, through the one token
1773+
* grammar and scope `onSuccess.navigate` reads (`substituteScopeTokens`).
1774+
* Only the sink differs: a toast is text, so a value is inserted as-is, where
1775+
* a URL position percent-encodes it.
1776+
*
1777+
* Rung 2 is a `template` slot (objectui#13367, the renderer half of
17731778
* objectstack#24814): a string or the `{ dialect: 'template', source }`
17741779
* envelope. A sentence with a `{{ }}` hole is filled by the template engine
1775-
* over `result` (the same payload) and `record` (the record the action ran
1776-
* against), and is then not read for `${…}` tokens; one without keeps the
1777-
* `${result.*}` reading above until the spec card converts the platform's
1778-
* own `${result.email}`-style sentences.
1780+
* over `result` and `record` (the record the action ran against). A
1781+
* sentence with none is shown as written: the transitional `${result.*}`
1782+
* reading it kept is gone (objectui#13476), and objectstack's publish gate
1783+
* refuses a `${…}` token in this copy.
17791784
*
17801785
* ⛔ `result.data.message` is not a rung. It used to outrank both rungs of
17811786
* author copy, which put whatever language the server happened to write in
17821787
* front of the user's locale; the contract names no server-message rung, and
17831788
* an author who wants the server's sentence can still write
1784-
* `${result.message}` as the copy.
1789+
* `{{ result.message }}` as the copy.
17851790
*
17861791
* A rung counts only when it is a non-empty string (for rung 2, or an
17871792
* envelope whose `source` is one). An `I18nLabel` map that
@@ -1807,7 +1812,7 @@ export class ActionRunner {
18071812
if (successCopy !== undefined && successCopy.trim() !== '') {
18081813
return hasTemplateHoles(successCopy)
18091814
? fillTemplateHoles(successCopy, { result: payload, record: this.dispatchRecord(action) })
1810-
: substituteScopeTokens(successCopy, { result: payload }, (value) => value);
1815+
: successCopy;
18111816
}
18121817
return this.runnerText(RUNNER_TEXT.completedSuccessfully);
18131818
}
@@ -1821,25 +1826,22 @@ export class ActionRunner {
18211826
* the record the action runs against, the same one `templateEvaluatorFor`
18221827
* binds — and `params`, the action's static params object when it has one
18231828
* (the dialog's collected values do not exist yet: the confirmation comes
1824-
* first). A sentence with no `{{ }}` hole keeps the transitional `${…}`
1825-
* reading through `templates`, so the platform's own `${…}` copy works
1826-
* until the spec card converts it.
1829+
* first). A sentence with no `{{ }}` hole is shown as written: a `${…}`
1830+
* token in it is text, not an expression (objectui#13476 retired the
1831+
* transitional `${…}` reading; objectstack's publish gate refuses the token).
18271832
*
18281833
* A declared value that carries no text — an envelope with no string
18291834
* `source` — still gates the run: the author declared a confirmation, and
18301835
* dropping it would let a destructive action run unasked. The dialog then
18311836
* shows no body text, never the object, which as a React child crashes the
18321837
* page. Every answer is a string for the same reason.
18331838
*/
1834-
private confirmMessage(action: ActionDef, templates: ExpressionEvaluator): string | undefined {
1839+
private confirmMessage(action: ActionDef): string | undefined {
18351840
const declared: unknown = action.confirmText;
18361841
if (!declared) return undefined;
18371842
const text = actionCopySource(declared);
18381843
if (text === undefined) return '';
1839-
if (!hasTemplateHoles(text)) {
1840-
const evaluated: unknown = templates.evaluate(text);
1841-
return typeof evaluated === 'string' ? evaluated : evaluated == null ? '' : String(evaluated);
1842-
}
1844+
if (!hasTemplateHoles(text)) return text;
18431845
const params = action.params;
18441846
const staticParams = params && typeof params === 'object' && !Array.isArray(params)
18451847
? Object.fromEntries(Object.entries(params).filter(([key]) => key !== '_rowRecord'))
@@ -2815,10 +2817,10 @@ export class ActionRunner {
28152817
// there would widen the authorable surface with nothing behind it and
28162818
// silently blank the token instead of leaving the author's mistake visible.
28172819
//
2818-
// The success toast's copy is the one other caller of the grammar, through
2819-
// `substituteScopeTokens` directly (`composeSuccessMessage`): `result` only,
2820-
// because that is the one scope the contract declares for the copy, and no
2821-
// encoding, because a toast is text.
2820+
// The success toast's `outcomeMessages` copy is the one other caller of the
2821+
// grammar, through `substituteScopeTokens` directly (`composeSuccessMessage`):
2822+
// `result` only, because that is the one scope the contract declares for the
2823+
// copy, and no encoding, because a toast is text.
28222824
const scopes: Record<string, unknown> = { param: params, ctx: this.buildInterpolationContext() };
28232825
if (resultScope !== undefined) scopes.result = resultScope;
28242826
return substituteScopeTokens(target, scopes, encodeURIComponent);
@@ -2866,9 +2868,10 @@ export class ActionRunner {
28662868
}
28672869

28682870
/**
2869-
* The evaluator a dispatch's authored copy is read through: `confirmText`,
2870-
* and the param dialog's `description` / `helpText` via the handler's
2871-
* `interpolate` argument.
2871+
* The evaluator a dispatch's authored copy is read through: the param
2872+
* dialog's `description` / `helpText`, via the handler's `interpolate`
2873+
* argument. (`confirmText` reads `{{ }}` holes only, over `dispatchRecord`,
2874+
* the same record this binds — see `confirmMessage`.)
28722875
*
28732876
* One rule for every door (objectui#12967): the record the action runs
28742877
* against is `record`. The record header binds it in the runner's context
@@ -2999,7 +3002,7 @@ export function readOnSuccessNavigation(value: unknown): OnSuccessNavigation | n
29993002
/**
30003003
* The runner's ONE `${scope.path}` token grammar — the substitution behind
30013004
* `interpolateTarget` (url / api targets and the `onSuccess.navigate` hop) and
3002-
* behind the success toast's `${result.*}` copy (`composeSuccessMessage`).
3005+
* behind the success toast's `outcomeMessages` copy (`composeSuccessMessage`).
30033006
*
30043007
* The scope MAP defines the vocabulary: the pattern is built from its keys, so
30053008
* a token naming a scope the caller did not pass is left in the text exactly as

‎packages/core/src/actions/__tests__/ActionRunner.callContext-13063.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('execute with a call context (objectui#13063)', () => {
8787
runner.setConfirmHandler(confirm);
8888

8989
const result = await runner.execute(
90-
{ type: 'form', target: 'task.edit', confirmText: 'Log time on ${record.name}?' },
90+
{ type: 'form', target: 'task.edit', confirmText: 'Log time on {{ record.name }}?' },
9191
{ record: ROW, objectName: 'task' },
9292
);
9393
expect(result.success).toBe(false);

‎packages/core/src/actions/__tests__/ActionRunner.copyTemplate-13367.test.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
* - a plain string, and an envelope, for each key;
2525
* - each declared root: `confirmText` → `record`, `params`;
2626
* `successMessage` → `result`, `record`; `errorMessage` → none (verbatim);
27-
* - the transition: a `${…}` sentence is still filled as before;
27+
* - no `${…}` reading: a sentence with no `{{ }}` hole is shown as written,
28+
* `${…}` tokens included (objectui#13476 retired the transitional
29+
* reading this card kept);
2830
* - one dialect per sentence: a value filled into a `{{ }}` hole is never
2931
* read again as `${…}`;
3032
* - a non-string never reaches the confirm handler or the toast.
@@ -88,8 +90,10 @@ describe('confirmText reads the envelope and fills {{ }} over record and params
8890
expect(await confirmTextFor(runner, { ...BASE, confirmText: envelope('By {{ user.name }}{{ result.id }}.') })).toBe('By .');
8991
});
9092

91-
it('transition: a `${…}` sentence is still read through the expression evaluator', async () => {
92-
expect(await confirmTextFor(header(), { ...BASE, confirmText: 'Archive ${record.name}?' })).toBe('Archive Acme?');
93+
it('a `${…}` token in a sentence with no hole is text, not an expression (objectui#13476)', async () => {
94+
expect(await confirmTextFor(header(), { ...BASE, confirmText: 'Archive ${record.name}?' })).toBe('Archive ${record.name}?');
95+
const row = { ...BASE, confirmText: envelope('Archive ${record.name}?'), params: { _rowRecord: { ...ROW } } };
96+
expect(await confirmTextFor(list(), row)).toBe('Archive ${record.name}?');
9397
});
9498

9599
it('one dialect per sentence: a record value is never evaluated as `${…}` after filling a hole', async () => {
@@ -165,9 +169,9 @@ describe('successMessage reads the envelope and fills {{ }} over result and reco
165169
expect(toastText()).toBe('Invitation sent');
166170
});
167171

168-
it('transition: the platform objects\' `${result.email}` sentence is still filled', async () => {
172+
it('a `${result.*}` token in a sentence with no hole is text, not filled (objectui#13476)', async () => {
169173
await runner.execute({ type: 'script', name: 'invite', successMessage: 'Invitation sent to ${result.email}' });
170-
expect(toastText()).toBe('Invitation sent to ann@example.com');
174+
expect(toastText()).toBe('Invitation sent to ${result.email}');
171175
});
172176

173177
it('one dialect per sentence: a filled value is not read again as `${result.*}`', async () => {

0 commit comments

Comments
 (0)