Skip to content

Commit a8ee8a2

Browse files
fix(studio): keep a refused publish on its Pending changes entry, and end "Show me" on the input (#26994)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ81WNJr1LMBXbQEggU6VRywj)_ Fixes #26730 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Studio 发布被拒后,原因留在「待发布的变更」对应条目上直到修复,用作者能看懂的话说;「Show me」把焦点落到出问题的输入框。 - 为什么改:以前拒绝只是约 4 秒的提示,里面是引擎内部文字,面板什么也不留,「Show me」也不聚焦。 - 风险与回滚:只改 Studio 前端与文案,不动发布判定、暂存编辑保护和服务端文字;回滚即还原本 PR。 - 先看:StudioDesignSurface.tsx 的 doPublish 与 useShowInputFocus;metadataError.ts 的 publishRefusal;DraftChangesPanel.tsx 的 renderEntryNote。 - 两分钟验证:运行 Verification 节第一条命令,应见 7 个测试文件、26 个测试全部通过。 - 需要你做的:无,席位落地。 ## What changes - **The refusal stays on its entry.** `doPublish` keeps each causal failure of a refused publish (every `failed[]` element that is not a `BATCH_ABORTED` sibling, per ADR-0067 D2) in surface state, keyed by canonical type and name. The surface hands the Pending changes sheet a new `renderEntryNote` prop, and the entry shows the refusal strip under its row until it is resolved: the draft is saved again (`onDraftSaved` now names the draft it wrote), a later publish lets it through, or the ledger no longer lists the draft. The package-less scope keeps its refusals the same way. - **Worded by the refusal reader.** New `publishRefusal` in `metadataError.ts` places a failure's issues with the locators a refused save uses. A gate issue path is rooted at the snapshot entry (`objects.NAME.…`, `flows[0].…`), so the first two segments are dropped. The draft the publish judged is read back (`getDraft`, best-effort) to place them. The sentence carries the server's problem only where the designer has its own words for it (`translateValidationMessage`), otherwise it says Details tell what to change. The raw lines stay under Details, verbatim. A failure with no issues reads `Not published: REASON`, with its text unchanged. - **The toast points at the sheet.** Its title counts the items that need a change, its description names each with its sentence, and a rolled-back batch adds one line saying the other drafts wait. It quotes no engine text. - **"Show me" ends on the input.** `useShowInputFocus` focuses the control the inspector labels with the refusal's `input` (or the inspector's first control when it names none), once the selection is drawn, giving up after 3 s. This applies in Data (field inspector, actions panel), Automations (step configuration) and Interfaces. Interfaces had no "Show me": a dashboard widget and a report's own inputs are now placed (`leafIssueLocator`), and "Show me" opens the widget, or the report's properties, with the focus on the input. - **Rows.** The `studio` namespace gains the sentences in `en` and `zh`, shipped as a patch changeset for `@object-ui/i18n`. ## Why The card measured three gaps, each confirmed before this change. A refused publish showed only a ~4 s toast of `formatPublishFailures` text, which quoted the functional-completeness rule's engine prose. Nothing kept `failed[]` after the toast. "Show me" selected its target but never moved the focus, and the Interfaces strip had no "Show me" at all. The ruling (6447119250) asks for the refusal to stay in the panel until resolved, in author terms, with the field focused. ## Tests - `metadataError.publishRefusal-26730.test.ts`: the summary-field rule's engine text is placed on the field and its input with no engine text in the sentence; Details keep the raw line; flow (`flows[0]`), dashboard widget and report placements; no draft means an unlocated sentence; an issue-less failure reads as its text; `zh` sentence after `loadStudioCatalogue('zh')`; only causal failures are kept. - `StudioDesignSurface.publishRefusal-26730.test.tsx` (real surface, sheet probe, toasts mocked): a refused publish leaves the refusal on its entry, worded by the reader, with the raw text under Details; the aborted sibling has none; re-saving clears it; a publish that lets it through clears it; a successful publish shows no refusal. - `DraftChangesPanel.entryNote-26730.test.tsx`: the real sheet draws the host's note under the right entry, asked with the folded type; no host note, no note. - `DataPillar.showMeFocus-26730.test.tsx`, `AutomationsPillar.showMeFocus-26730.test.tsx`, `InterfacesPillar.showMeFocus-26730.test.tsx`: "Show me" leaves `document.activeElement` on the named input: a field's Label, an action's label from another view, a step's Title, a step's inspector, a dashboard widget's Title, a report's Label. - `StudioDesignSurface.publishCount-11807.test.tsx` updated: the refused batch's toast title is now the count sentence, and the item and its reason are in the description. ## Acceptance notes - The note on a Pending changes entry has no "Show me": the sheet reaches an item through its own link, and a strip's "Show me" opens an input only in the pillar that holds the item. The pillars' strips carry it. - Held edits (objectui#11786) keep their notice and their "Show me" as before. - Refusals are keyed by canonical type and name, so two members of one bundle (an `email_template` pair) share one note. - A `BATCH_ABORTED` sibling gets no note: it publishes once the causal item is fixed. - No change to publish gating, the held-edit guard, server text, or `formatMetadataError`. `formatPublishFailures` is unchanged; the AI chat page still uses it. - Template exception: the body follows the os-dev section order, with the template's headings (What changes, Why, Verification, the two declaration lines, Human Minimum) folded in. ## Verification Head `330e5fef`, base `5ec398dc`; the branch merges cleanly with the current `main`. 1. The new and updated tests: `pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/metadataError.publishRefusal-26730.test.ts packages/app-shell/src/views/studio-design/StudioDesignSurface.publishRefusal-26730.test.tsx packages/app-shell/src/preview/__tests__/DraftChangesPanel.entryNote-26730.test.tsx packages/app-shell/src/views/studio-design/AutomationsPillar.showMeFocus-26730.test.tsx packages/app-shell/src/views/studio-design/DataPillar.showMeFocus-26730.test.tsx packages/app-shell/src/views/studio-design/InterfacesPillar.showMeFocus-26730.test.tsx packages/app-shell/src/views/studio-design/StudioDesignSurface.publishCount-11807.test.tsx` gives Test Files 7 passed (7), Tests 26 passed (26). 2. Every Studio and preview suite at the head: `pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design packages/app-shell/src/preview` gives Test Files 274 passed (274), Tests 1950 passed (1950). 3. `@object-ui/i18n`: `tsc --noEmit` exit 0; `vitest run packages/i18n` gives Test Files 89 passed (89), Tests 1373 passed, 13 skipped. 4. `@object-ui/app-shell`: `tsc --noEmit` and `tsc -p tsconfig.test.json` exit 0; eslint over the touched files reports 0 errors, and its one new warning (a state write in an effect) was removed by deriving the shown refusals instead. <details><summary>Ablations (2, each through scripts/ablation-replace.mjs with a restore trap)</summary> - Drop the kept-failures state (delete the `setPublishRefusals(new Map(…))` line in `doPublish`): `StudioDesignSurface.publishRefusal-26730.test.tsx` goes red, 3 failed and 1 passed (the passing one is the control, a successful publish shows no refusal). Restored: blob equals HEAD, `git diff HEAD` empty. - Drop the focus move (delete `control.focus();` in `useShowInputFocus`): the three showMeFocus files go red, 6 of 6 failed. Restored: blob equals HEAD, `git diff HEAD` empty. </details> <details><summary>Dispatch gates: 34 derived, 34 run, all exit 0; --ran reconciled with 0 unrun and 0 not measured</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` untouched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; eslint over the touched files reports 0 errors. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English, except the maintainer summary the os-dev contract writes in Chinese. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbaQ2uD68Bnvz94D9D1)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent d7df639 commit a8ee8a2

13 files changed

Lines changed: 1755 additions & 144 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/i18n': patch
3+
---
4+
5+
feat(i18n): the `studio` namespace words a refused publish, and a refusal on a dashboard widget or a report input
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Rows for objectstack#26730.** Studio keeps a refused publish on its Pending changes entry until it is fixed, and its "Show me" now ends on the input a refusal names. The namespace gains the sentences for that: a publish refusal (`engine.studio.refusal.publishIssue`, `publishWhere`, `publishPlain`, `publishUnlocated`), the toast that points at the sheet (`engine.studio.publishRefused`, `publishRefusedOne`, `publishRefusedBatch`), and where a dashboard widget or a report's own input is named (`engine.studio.refusal.widget`, `widgetInput`, `reportInput`). Each row has its `zh` text. No existing row changes, and no export changes.

‎ui/packages/app-shell/src/preview/DraftChangesPanel.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,14 @@ export interface DraftChangesPanelProps {
946946
* publishes).
947947
*/
948948
posture?: DraftChangesPosture;
949+
/**
950+
* objectstack#26730 — the host's note on one entry, shown under its row
951+
* whether or not the row is expanded: Studio keeps a refused publish's
952+
* reason here until it is resolved. Rendered by the host, so the reader that
953+
* words it stays off this panel's eager graph. `null` for an entry with
954+
* nothing to say.
955+
*/
956+
renderEntryNote?: (entry: DraftChangeEntry) => ReactNode;
949957
}
950958

951959
export type DraftChangesPosture = 'review' | 'confirm';
@@ -958,6 +966,7 @@ export function DraftChangesPanel({
958966
onPublish,
959967
publishing = false,
960968
posture = 'review',
969+
renderEntryNote,
961970
}: DraftChangesPanelProps) {
962971
const { t, language } = useObjectTranslation();
963972
const [entries, setEntries] = useState<DraftChangeEntry[] | null>(null);
@@ -1268,6 +1277,7 @@ export function DraftChangesPanel({
12681277
// secondary text, or is the row's text when there is no
12691278
// label.
12701279
const labelText = resolveI18nLabel(entry.label, language)?.trim() ?? '';
1280+
const note = renderEntryNote?.(entry);
12711281
return (
12721282
<li key={key} className="rounded-md border text-sm">
12731283
<button
@@ -1336,6 +1346,11 @@ export function DraftChangesPanel({
13361346
</Badge>
13371347
) : null}
13381348
</button>
1349+
{note ? (
1350+
<div className="border-t px-2.5 py-2" data-testid="draft-entry-note">
1351+
{note}
1352+
</div>
1353+
) : null}
13391354
{isExpanded && (
13401355
<div className="border-t bg-muted/20">
13411356
<EntryDetail
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#26730 — a Pending changes entry carries the note its host gives it.
5+
*
6+
* The sheet is on the console's eager graph, so it does not read a refusal
7+
* itself: the Studio surface hands it `renderEntryNote`, and each entry shows
8+
* what that answers under its row, with every entry the host has nothing for
9+
* drawn as before. The surface's side (which refusal, when it goes) is pinned
10+
* in `StudioDesignSurface.publishRefusal-26730.test.tsx`.
11+
*/
12+
13+
import '@testing-library/jest-dom/vitest';
14+
import * as React from 'react';
15+
import { afterEach, describe, expect, it, vi } from 'vitest';
16+
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
17+
18+
vi.mock('@object-ui/i18n', async (importOriginal) => ({
19+
...(await importOriginal<Record<string, unknown>>()),
20+
useObjectTranslation: () => ({
21+
t: (_k: string, o?: { defaultValue?: string }) => o?.defaultValue ?? _k,
22+
language: 'en',
23+
}),
24+
}));
25+
26+
import { DraftChangesPanel, type DraftChangeEntry } from '../DraftChangesPanel';
27+
28+
// `flows`: a stored plural spelling, which the sheet folds before the host sees it.
29+
const DRAFT_ROWS = [
30+
{ type: 'flows', name: 'approval', packageId: 'com.x', label: 'Approval' },
31+
{ type: 'object', name: 'invoice', packageId: 'com.x', label: 'Invoice' },
32+
];
33+
34+
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
35+
const url = String(input);
36+
const ok = (body: unknown) => ({ ok: true, status: 200, json: async () => body });
37+
if (url.includes('/_drafts')) return ok({ drafts: DRAFT_ROWS });
38+
if (/\/meta\/[a-z_]+(\?|$)/.test(url)) return ok([]);
39+
return { ok: false, status: 404, json: async () => ({}) };
40+
});
41+
42+
afterEach(() => {
43+
cleanup();
44+
vi.unstubAllGlobals();
45+
fetchMock.mockClear();
46+
});
47+
48+
async function renderPanel(renderEntryNote?: (entry: DraftChangeEntry) => React.ReactNode) {
49+
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch);
50+
render(<DraftChangesPanel open onOpenChange={() => {}} packageId="com.x" renderEntryNote={renderEntryNote} />);
51+
await waitFor(() => expect(screen.getAllByTestId('draft-entry-toggle')).toHaveLength(DRAFT_ROWS.length));
52+
}
53+
54+
/** The entry whose row holds `text`. */
55+
function entryOf(text: string): HTMLElement {
56+
return screen.getByText(text).closest('[data-testid="draft-entry-toggle"]')!.parentElement as HTMLElement;
57+
}
58+
59+
describe('a Pending changes entry shows the note its host gives it (objectstack#26730)', () => {
60+
it('under the entry it is for, and only there', async () => {
61+
const seen: string[] = [];
62+
await renderPanel((entry) => {
63+
seen.push(`${entry.type}/${entry.name}`);
64+
return entry.name === 'approval' ? <p>Not published. Check the step “Notify”.</p> : null;
65+
});
66+
// The host is asked with the folded type.
67+
expect(seen).toContain('flow/approval');
68+
const note = within(entryOf('Approval')).getByTestId('draft-entry-note');
69+
expect(note).toHaveTextContent('Not published. Check the step “Notify”.');
70+
expect(within(entryOf('Invoice')).queryByTestId('draft-entry-note')).toBeNull();
71+
});
72+
73+
it('CONTROL: with no host note, no entry draws one', async () => {
74+
await renderPanel();
75+
expect(screen.queryByTestId('draft-entry-note')).toBeNull();
76+
});
77+
});
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#26730 — the Automations strip's "Show me" ends on the input.
5+
*
6+
* Measured before this card: "Show me" selected the step the refusal names
7+
* and stopped there, so the focus stayed on the button and the author still
8+
* had to find the input. It now ends on the input the sentence names: the
9+
* step's Title for `nodes.2.config.title`, and the step's first input when
10+
* the sentence names the step alone.
11+
*
12+
* The harness is `AutomationsPillar.authorRefusal-11785.test.tsx`'s: the real
13+
* registered `FlowPreview` and `FlowInspector`, and a server double whose
14+
* `save` refuses, driven through the header switch.
15+
*/
16+
17+
import '@testing-library/jest-dom/vitest';
18+
import * as React from 'react';
19+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
20+
import { render, screen, fireEvent, cleanup, waitFor, within } from '@testing-library/react';
21+
import { MemoryRouter } from 'react-router-dom';
22+
23+
const PKG = 'com.acme.app';
24+
25+
const FLOW = {
26+
name: 'approval',
27+
label: 'Approval',
28+
type: 'autolaunched',
29+
status: 'active',
30+
nodes: [
31+
{ id: 'start', type: 'start', label: 'Start' },
32+
{ id: 'route', type: 'decision', label: 'Route' },
33+
{ id: 'tell', type: 'notify', label: 'Notify approver', config: { recipients: ['owner'] } },
34+
{ id: 'end', type: 'end', label: 'End' },
35+
],
36+
edges: [
37+
{ id: 'e1', source: 'start', target: 'route' },
38+
{ id: 'e2', source: 'route', target: 'tell' },
39+
{ id: 'e3', source: 'tell', target: 'end' },
40+
],
41+
};
42+
43+
const MISSING = 'Invalid input: expected string, received undefined';
44+
45+
const server = vi.hoisted(() => ({
46+
/** The issues the next draft save is refused with. */
47+
issues: [] as Array<{ path: string; message: string }>,
48+
}));
49+
50+
const mockClient = vi.hoisted(() => ({
51+
list: vi.fn(async () => [{ name: 'approval', label: 'Approval' }]),
52+
listDrafts: vi.fn(async () => []),
53+
listTypes: vi.fn(async () => ({ entries: [] })),
54+
get: vi.fn(async () => null),
55+
references: vi.fn(async () => []),
56+
layered: vi.fn(async () => ({
57+
code: null,
58+
overlay: null,
59+
overlayScope: null,
60+
effective: JSON.parse(JSON.stringify(FLOW)),
61+
editable: true,
62+
deletable: true,
63+
resettable: false,
64+
lock: 'none',
65+
})),
66+
getDraft: vi.fn(async (type: string, name: string) => {
67+
throw Object.assign(new Error(`No pending draft exists for ${type}/${name}.`), { code: 'NO_DRAFT', status: 404 });
68+
}),
69+
save: vi.fn(async () => {
70+
// The client's parsed `MetadataError`: a headline, a status, a code and
71+
// the structured issues the server returned.
72+
throw Object.assign(new Error(`flow/approval failed spec validation (${server.issues.length})`), {
73+
status: 422,
74+
code: 'INVALID_METADATA',
75+
issues: server.issues,
76+
});
77+
}),
78+
publish: vi.fn(async () => ({ success: true })),
79+
reset: vi.fn(async () => ({})),
80+
}));
81+
82+
vi.mock('../metadata-admin/useMetadata', async (importOriginal) => {
83+
const mod = await importOriginal<typeof import('../metadata-admin/useMetadata')>();
84+
return { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ entries: [] }) };
85+
});
86+
87+
vi.mock('./packages-io', async (importOriginal) => {
88+
const mod = await importOriginal<typeof import('./packages-io')>();
89+
return { ...mod, fetchPackages: vi.fn(async () => []) };
90+
});
91+
92+
vi.mock('@object-ui/react', async (importOriginal) => {
93+
const mod = await importOriginal<typeof import('@object-ui/react')>();
94+
return { ...mod, useAdapter: () => dataSource };
95+
});
96+
97+
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
98+
99+
import { AutomationsPillar } from './StudioDesignSurface';
100+
import { createEmptyDataSource } from './__tests__/emptyDataSource';
101+
import { registerMetadataPreview } from '../metadata-admin/preview-registry';
102+
import { registerMetadataInspector } from '../metadata-admin/inspector-registry';
103+
import { FlowPreview } from '../metadata-admin/previews/FlowPreview';
104+
import { FlowInspector } from '../metadata-admin/inspectors/FlowInspector';
105+
import { t } from '../metadata-admin/i18n';
106+
107+
const dataSource = createEmptyDataSource();
108+
109+
// The pillar's `/automation/_status` probe and the inspector's catalog reads go
110+
// through the global `fetch`; "absent" keeps each on its documented fallback.
111+
vi.stubGlobal(
112+
'fetch',
113+
vi.fn(async () => new Response('null', { status: 404, headers: { 'content-type': 'application/json' } })),
114+
);
115+
116+
registerMetadataPreview('flow', FlowPreview);
117+
registerMetadataInspector('flow', FlowInspector);
118+
119+
beforeEach(() => {
120+
server.issues = [];
121+
for (const fn of Object.values(mockClient)) (fn as unknown as { mockClear: () => void }).mockClear();
122+
});
123+
124+
afterEach(cleanup);
125+
126+
/** Open the flow, then flip the header switch: a save that the server refuses. */
127+
async function refusedSave(issues: Array<{ path: string; message: string }>): Promise<HTMLElement> {
128+
server.issues = issues;
129+
render(
130+
<MemoryRouter initialEntries={[`/studio/${PKG}/automations`]}>
131+
<AutomationsPillar packageId={PKG} />
132+
</MemoryRouter>,
133+
);
134+
const toggle = await screen.findByRole('switch', undefined, { timeout: 8000 });
135+
await waitFor(() => expect(toggle).not.toBeDisabled());
136+
fireEvent.click(toggle);
137+
await waitFor(() => expect(mockClient.save).toHaveBeenCalledTimes(1));
138+
return screen.findByTestId('studio-refusal');
139+
}
140+
141+
describe('Automations page — "Show me" puts the focus on the input the refusal names (objectstack#26730)', () => {
142+
it('`nodes.2.config.title`: the focus lands on the step\'s Title', async () => {
143+
const strip = await refusedSave([{ path: 'nodes.2.config.title', message: MISSING }]);
144+
const show = within(strip).getByRole('button', { name: t('engine.studio.refusal.show', 'en') });
145+
show.focus();
146+
fireEvent.click(show);
147+
148+
const rail = screen.getByTestId('auto-config-aside');
149+
// The Title box, by the placeholder the notify table gives it (its label
150+
// heads the row rather than naming the box).
151+
const title = await within(rail).findByPlaceholderText('Your request was approved', undefined, { timeout: 8000 });
152+
await waitFor(() => expect(document.activeElement).toBe(title));
153+
expect(within(rail).getByLabelText('ID')).toHaveValue('tell');
154+
});
155+
156+
it('a sentence naming the step alone: the focus lands in that step\'s inspector', async () => {
157+
const strip = await refusedSave([{ path: 'nodes.2', message: MISSING }]);
158+
fireEvent.click(within(strip).getByRole('button', { name: t('engine.studio.refusal.show', 'en') }));
159+
160+
const rail = screen.getByTestId('auto-config-aside');
161+
await within(rail).findByLabelText('ID', undefined, { timeout: 8000 });
162+
await waitFor(() => expect(rail.contains(document.activeElement)).toBe(true));
163+
});
164+
});

0 commit comments

Comments
 (0)