Skip to content

Commit 987ef87

Browse files
fix(app-shell): a read-only flow edge inspector says why its controls are disabled (#29206)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbHnTjnh5M3LadfnDnbjjaUP)_ Fixes #29077 Clause-②: no Metadata-protocol: none Claim: issue comment 6572861977 (branch `claude/project-thread-1r2gue`, session `session_01QsR2jU7NRnC2NpRec6yEYE`). ## 维护者速读 - **改了什么**:Studio 流程画布上,只读包里选中一条连线后,连线属性栏现在会说明控件为什么是灰的:悬停任一灰色控件弹出原因,键盘 Tab 能停在属性栏上,读屏把原因当描述读出。与节点属性栏(#28985)同一做法,两边共用一个新的共享组件,不再各写一份。 - **为什么改**:宿主早已把「只读软件包」的原因传给连线属性栏,但它没读,控件灰着却什么也不说;平台清单 `ux-conventions.disabled-control-reason` 在这个面上仍判失败。 - **风险与回滚**:低。只有「只读且宿主给了原因」时才多包一层可聚焦分组;不传原因的宿主(如元数据编辑页)与可写状态完全不变。节点属性栏改用共享组件后 DOM 不变,其原有测试未改即通过。回滚即还原本 PR。 - **先看**:`_shared.tsx` 新增的 `InspectorReadOnlyReason`;`FlowEdgeInspector.tsx` 末尾的包裹;`FlowNodeInspector.tsx` 换成共享组件的那一段。 - **两分钟验证**:在 `ui/` 下运行 `pnpm exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/FlowEdgeInspector.readOnlyReason-29077.test.tsx packages/app-shell/src/views/studio-design/StudioDesignSurface.automationsReadOnly-11124.test.tsx`,预期 2 个文件、17 个测试全绿。 - **需要你做的**:无,席位落地。 ## What changes - **`InspectorReadOnlyReason`** (new, `inspectors/_shared.tsx`; not reachable from the `@object-ui/app-shell` entry). The reason group `FlowNodeInspector` drew inline since #28985, lifted so both flow inspectors draw one: a `role="group"` named by the element the panel edits, `tabIndex={0}`, `aria-describedby` an `sr-only` copy of the reason, `data-disabled-reason`, and the trigger of a tooltip that shows it. It renders its children bare unless `readOnly` and the reason is non-blank. - **`FlowEdgeInspector`** now reads `readOnlyReason` and wraps its panel in that group, named by the connection the way its title is (each end by its node's name, never an id). The missing-edge empty state has no controls and stays bare. - **`FlowNodeInspector`** uses the shared group with the same DOM, test id and name it had; its own `useId` and tooltip imports go. - Studio's Automations pillar already hands every flow inspector the reason (#28985), so no host changes. `ResourceEditPage` still passes none (#29079). ## Tests - **New: `FlowEdgeInspector.readOnlyReason-29077.test.tsx`, 6 cases** on an approval's out-edge (so the Approval branch picker draws too): read-only with a reason makes the panel a tab stop named by the connection and described by the reason, holding every disabled control (at least six); hovering the disabled "Remove connection" opens the tooltip; Tab lands on the panel and opens it. Controls: no reason gives no group and no tab stop; a blank reason counts as none; a writable panel ignores a reason and its controls are live. - **Changed: `StudioDesignSurface.automationsReadOnly-11124.test.tsx`**, one new case: through the real pillar, `FlowPreview` and `FlowInspector`, clicking the start → end connection on a read-only package opens the edge inspector inside the described group with the package's reason. - **Red on main.** With the three source files restored from base `0d6d4061`, the two edge files plus the #28985 node file ran 4 failed, 19 passed: the three edge reason cases and the host edge case fail (no `flow-edge-readonly-reason` group), every control and every node case passes. ## Acceptance notes - Other surfaces still inline the same group (Studio's field rail, the validation-rule editor, the flow ledger switch, the field inspector's per-control locks); they are not moved here. Each can adopt `InspectorReadOnlyReason` when next touched. - The tooltip also opens when hovering the header's close button, since the whole panel is the trigger; the node inspector behaves the same. ## Verification All readings on head `f844add6` unless named (fix `07c2c014`, title follow-up `f844add6`), base `0d6d4061`. <details><summary>Tests</summary> - `pnpm exec vitest run` (from `ui/`) on the two edge files plus `FlowNodeInspector.readOnlyReason-28985.test.tsx`: 3 files, 23 tests passed. - Red on main: the same three files with `_shared.tsx`, `FlowNodeInspector.tsx` and `FlowEdgeInspector.tsx` restored from `0d6d4061`: 4 failed, 19 passed (the three edge reason cases and the host edge case, each on the missing group; the #28985 node cases pass, so the node DOM is unchanged). Restored with `git checkout HEAD`, `git diff HEAD` empty. - Sweep of the 120 app-shell test files that `git grep` finds naming `FlowEdgeInspector`, `FlowNodeInspector`, `FlowInspector`, `AutomationsPillar`, `InspectorShell`, `_shared` or `flowNodeLabels`, in three locked batches at `--maxWorkers=2`: 38/38 files (425 tests), 42/42 with the new file (580), 41/41 (339). The first run on `07c2c014` caught `flowNodeLabels.sites-13200.test.ts`, which pins the edge title's `flowEdgeDisplayName` expression in place; `f844add6` keeps it there, and all three batches re-ran green on `f844add6`. Narrowed on purpose: CI's Test UI runs the rest of app-shell. - `pnpm exec turbo run type-check --filter=@object-ui/app-shell`: 65 of 65 tasks successful. - `eslint` on the five changed ui files: 0 errors (13 existing warnings, all on untouched lines). </details> <details><summary>Gates</summary> - `node scripts/pm/dispatch-gates.mjs --commands` derived 34 families; all 34 ran with exit 0, the unlocked ones with this body as the PR event (`check-adr-0087-registration`, `check-changeset-no-major`, `check-closing-keyword-parity`, `check-sdui-manifest` among them), and `pnpm --dir ui exec vitest run scripts/` locked: 169 files passed, 2 skipped (4,900 tests). - `dispatch-gates --ran`: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. - `check:new-line-citations`: 0 new citations. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [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: its generated artifacts are regenerated and committed. (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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jU7NRnC2NpRec6yEYE --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jU7NRnC2NpRec6yEYE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 67c2e3a commit 987ef87

6 files changed

Lines changed: 257 additions & 31 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): a read-only flow edge inspector says why its controls are disabled, on hover, on keyboard focus and to assistive tech
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
On a read-only package, Studio's flow connection inspector disabled every control (From, To, the branch pickers, Label, Condition, Default branch, Type, Remove connection) and gave no reason anywhere: no tooltip, no accessible description, and Tab skipped the whole panel. The reason Studio's Automations pillar hands every flow inspector reached it and was dropped. The connection inspector now carries the treatment the node inspector carries: the panel is a tab stop described by the reason, and hovering any disabled control in it shows the reason in a tooltip. Both inspectors draw it through one shared wrapper. A host that passes no reason renders exactly as before.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#29077 — a read-only flow edge inspector says why its controls are
5+
* disabled, on hover, on keyboard focus and to assistive tech, when its host
6+
* hands it the reason (`readOnlyReason`), as the node inspector does since
7+
* objectstack#28985.
8+
*
9+
* Measured on the card: on a read-only package every control of the connection
10+
* inspector (From, To, the branch pickers, Label, Condition, Default branch,
11+
* Type, Remove connection) was disabled with no `aria-describedby` and no tab
12+
* stop, and the reason Studio's Automations pillar hands every flow inspector
13+
* was dropped. Both panels now draw it through one `InspectorReadOnlyReason`.
14+
*
15+
* The reason is the host's: a host that passes none gets the panel it always
16+
* had. Studio's pillar passing it to a selected connection is pinned in
17+
* `StudioDesignSurface.automationsReadOnly-11124.test.tsx`.
18+
*/
19+
20+
import '@testing-library/jest-dom/vitest';
21+
import * as React from 'react';
22+
import { describe, it, expect, vi, afterEach } from 'vitest';
23+
import { render, screen, cleanup } from '@testing-library/react';
24+
import userEvent from '@testing-library/user-event';
25+
26+
vi.mock('../previews/useObjectFields', () => ({
27+
useObjectFields: () => ({ fields: [], loading: false, error: null }),
28+
}));
29+
30+
import { FlowEdgeInspector } from './FlowEdgeInspector';
31+
import { flowEdgeDisplayName } from '../previews/flow-canvas-parts';
32+
import { t } from '../i18n';
33+
34+
afterEach(cleanup);
35+
36+
const REASON = 'Read-only package: switch to or create a writable package to edit.';
37+
38+
const NODES = [
39+
{ id: 'start', type: 'start', label: 'Start' },
40+
{ id: 'review', type: 'approval', label: 'Review' },
41+
{ id: 'end', type: 'end', label: 'End' },
42+
];
43+
// An approval's out-edge, so the panel also draws its "Approval branch" picker.
44+
const DRAFT = {
45+
name: 'f',
46+
label: 'F',
47+
nodes: NODES,
48+
edges: [
49+
{ id: 'e1', source: 'start', target: 'review' },
50+
{ id: 'e2', source: 'review', target: 'end', label: 'approve' },
51+
],
52+
};
53+
54+
function mount({ readOnly, readOnlyReason }: { readOnly: boolean; readOnlyReason?: string }) {
55+
return render(
56+
<FlowEdgeInspector
57+
type="flow"
58+
name="f"
59+
draft={DRAFT}
60+
selection={{ kind: 'edge', id: 'e2' }}
61+
onPatch={() => {}}
62+
onClearSelection={() => {}}
63+
readOnly={readOnly}
64+
readOnlyReason={readOnlyReason}
65+
locale="en-US"
66+
/>,
67+
);
68+
}
69+
70+
const removeButton = () => screen.getByRole('button', { name: t('engine.inspector.flowEdge.remove', 'en-US') });
71+
72+
/** The panel's form controls, Remove connection included — the ones the reason is about. */
73+
const controls = (root: HTMLElement) => [
74+
...root.querySelectorAll<HTMLElement>('input, select, textarea, button[role="combobox"]'),
75+
removeButton(),
76+
];
77+
78+
describe('Flow edge inspector, read-only with a reason from its host (objectstack#29077)', () => {
79+
it('the panel is a tab stop described by the reason, named by the connection, and holds the disabled controls', () => {
80+
const { container } = mount({ readOnly: true, readOnlyReason: REASON });
81+
const body = container.querySelector('[data-disabled-reason]') as HTMLElement;
82+
expect(body).not.toBeNull();
83+
expect(body).toHaveAttribute('tabindex', '0');
84+
expect(body).toHaveAccessibleDescription(REASON);
85+
// The same name the panel's title shows: each end by its node's name.
86+
expect(body).toHaveAccessibleName(flowEdgeDisplayName({ source: 'review', target: 'end' }, NODES, 'en-US'));
87+
expect(body).not.toHaveAccessibleName(/review|end\b/);
88+
89+
const all = controls(body);
90+
// From, To, Approval branch, Label, Condition, Default branch, Type and
91+
// Remove connection; at least six, so a vacuous pass is not one.
92+
expect(all.length).toBeGreaterThanOrEqual(6);
93+
for (const el of all) {
94+
expect(body).toContainElement(el);
95+
expect(el).toBeDisabled();
96+
}
97+
});
98+
99+
it('hovering the disabled "Remove connection" opens a tooltip with the reason', async () => {
100+
const user = userEvent.setup();
101+
mount({ readOnly: true, readOnlyReason: REASON });
102+
expect(removeButton()).toBeDisabled();
103+
await user.hover(removeButton());
104+
expect(await screen.findByRole('tooltip')).toHaveTextContent(REASON);
105+
});
106+
107+
it('a keyboard user reaches the reason: Tab lands on the described panel, which opens the tooltip', async () => {
108+
const user = userEvent.setup();
109+
const { container } = mount({ readOnly: true, readOnlyReason: REASON });
110+
const body = container.querySelector('[data-disabled-reason]') as HTMLElement;
111+
await user.tab();
112+
expect(body).toHaveFocus();
113+
expect(await screen.findByRole('tooltip')).toHaveTextContent(REASON);
114+
});
115+
});
116+
117+
describe('Flow edge inspector — the controls (objectstack#29077)', () => {
118+
it('a host that passes no reason gets the panel it always had: no tab stop, no description', () => {
119+
const { container } = mount({ readOnly: true });
120+
expect(container.querySelector('[data-disabled-reason]')).toBeNull();
121+
expect(container.querySelector('[tabindex="0"]')).toBeNull();
122+
for (const el of controls(container)) expect(el).toBeDisabled();
123+
});
124+
125+
it('a blank reason counts as none', () => {
126+
const { container } = mount({ readOnly: true, readOnlyReason: ' ' });
127+
expect(container.querySelector('[data-disabled-reason]')).toBeNull();
128+
});
129+
130+
it('a writable inspector ignores a reason: the controls are live and nothing is described', () => {
131+
const { container } = mount({ readOnly: false, readOnlyReason: REASON });
132+
expect(container.querySelector('[data-disabled-reason]')).toBeNull();
133+
expect(screen.queryByText(REASON)).toBeNull();
134+
const all = controls(container);
135+
expect(all.length).toBeGreaterThanOrEqual(6);
136+
for (const el of all) expect(el).toBeEnabled();
137+
});
138+
});

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowEdgeInspector.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
InspectorCheckboxField,
3838
InspectorRemoveButton,
3939
InspectorEmptyState,
40+
InspectorReadOnlyReason,
4041
spliceArray,
4142
} from './_shared.js';
4243
import { Label } from '@object-ui/components';
@@ -73,6 +74,7 @@ export function FlowEdgeInspector({
7374
onSelectionChange,
7475
locale,
7576
readOnly,
77+
readOnlyReason,
7678
}: MetadataInspectorProps) {
7779
const edges = Array.isArray((draft as any).edges) ? ((draft as any).edges as FlowDesignerEdge[]) : [];
7880
const index = edges.findIndex((e, i) => edgeKey(e, i) === selection.id);
@@ -204,7 +206,7 @@ export function FlowEdgeInspector({
204206

205207
const edgeType = (typeof edge.type === 'string' && edge.type) || 'default';
206208

207-
return (
209+
const shell = (
208210
<InspectorShell
209211
kindLabel={t('engine.inspector.flowEdge.kind', locale)}
210212
// Read off the edge itself, each end by its node's name (objectui#13143):
@@ -376,4 +378,17 @@ export function FlowEdgeInspector({
376378
)}
377379
</InspectorShell>
378380
);
381+
382+
// Read-only with a reason from the host: every control above is disabled for
383+
// that one reason, so the panel carries it, as the node inspector's does.
384+
return (
385+
<InspectorReadOnlyReason
386+
readOnly={readOnly}
387+
reason={readOnlyReason}
388+
label={flowEdgeDisplayName(edge, nodes, locale)}
389+
testId="flow-edge-readonly-reason"
390+
>
391+
{shell}
392+
</InspectorReadOnlyReason>
393+
);
379394
}

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.tsx‎

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import * as React from 'react';
2323
import { Plus } from 'lucide-react';
2424
import { resolveFlowTriggerKind } from '@objectstack/spec/automation';
25-
import { Input, Label, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@object-ui/components';
25+
import { Input, Label } from '@object-ui/components';
2626
import type { MetadataInspectorProps } from '../inspector-registry.js';
2727
import { t, tFormat } from '../i18n.js';
2828
import {
@@ -31,6 +31,7 @@ import {
3131
InspectorSelectField,
3232
InspectorRemoveButton,
3333
InspectorEmptyState,
34+
InspectorReadOnlyReason,
3435
} from './_shared.js';
3536
import {
3637
mergeServerFlowFields,
@@ -294,7 +295,6 @@ export function FlowNodeInspector({
294295
readOnly,
295296
readOnlyReason,
296297
}: MetadataInspectorProps) {
297-
const readOnlyReasonId = React.useId();
298298
// Resolve the selection to a node + how to write it back — a top-level draft
299299
// node, or a node nested inside a container region (#2670). Every edit goes
300300
// through loc.write, so the inspector never branches on where the node lives.
@@ -895,34 +895,16 @@ export function FlowNodeInspector({
895895
</InspectorShell>
896896
);
897897

898-
const reason = readOnly ? readOnlyReason?.trim() : undefined;
899-
if (!reason) return shell;
900898
// Read-only with a reason from the host: every control above is disabled for
901-
// that one reason, and a disabled control takes neither hover nor focus. The
902-
// inspector body is the tooltip trigger, so hovering any of them shows it, and
903-
// a tab stop described by an `sr-only` copy, so a keyboard or screen-reader
904-
// user reaches it too. One trigger for the panel, not one per control: the
905-
// reason is the same for all of them. Same treatment as Studio's field rail
906-
// and validation-rule editor.
899+
// that one reason, so the panel carries it (`InspectorReadOnlyReason`).
907900
return (
908-
<TooltipProvider delayDuration={200}>
909-
<Tooltip>
910-
<TooltipTrigger asChild>
911-
<div
912-
role="group"
913-
aria-label={flowNodeDisplayName(node, locale)}
914-
tabIndex={0}
915-
aria-describedby={readOnlyReasonId}
916-
data-disabled-reason=""
917-
data-testid="flow-node-readonly-reason"
918-
className="flex h-full min-h-0 flex-col focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
919-
>
920-
{shell}
921-
<span id={readOnlyReasonId} className="sr-only">{reason}</span>
922-
</div>
923-
</TooltipTrigger>
924-
<TooltipContent side="left" align="start">{reason}</TooltipContent>
925-
</Tooltip>
926-
</TooltipProvider>
901+
<InspectorReadOnlyReason
902+
readOnly={readOnly}
903+
reason={readOnlyReason}
904+
label={flowNodeDisplayName(node, locale)}
905+
testId="flow-node-readonly-reason"
906+
>
907+
{shell}
908+
</InspectorReadOnlyReason>
927909
);
928910
}

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/_shared.tsx‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,21 @@
3030
import * as React from 'react';
3131
import { ArrowDown, ArrowUp, Trash2, X } from 'lucide-react';
3232
import { cn } from '@object-ui/components';
33-
import { Badge, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@object-ui/components';
33+
import {
34+
Badge,
35+
Button,
36+
Input,
37+
Label,
38+
Select,
39+
SelectContent,
40+
SelectItem,
41+
SelectTrigger,
42+
SelectValue,
43+
Tooltip,
44+
TooltipContent,
45+
TooltipProvider,
46+
TooltipTrigger,
47+
} from '@object-ui/components';
3448
import { type LoadState } from '../loadState.js';
3549
import { t, tFormat, useMetadataLocale, type SupportedLocale } from '../i18n.js';
3650

@@ -704,6 +718,53 @@ export function InspectorEmptyState({ message }: { message: string }) {
704718
return <div className="text-xs italic text-muted-foreground p-4 text-center">{message}</div>;
705719
}
706720

721+
export interface InspectorReadOnlyReasonProps {
722+
/** The inspector's `readOnly`: with it false, `children` render bare. */
723+
readOnly?: boolean;
724+
/** The host's `readOnlyReason`. Missing or blank, `children` render bare. */
725+
reason?: string;
726+
/** The group's accessible name: the element the panel edits. */
727+
label: string;
728+
testId?: string;
729+
children: React.ReactNode;
730+
}
731+
732+
/**
733+
* The reason a read-only inspector's controls are disabled, given on hover, on
734+
* keyboard focus and to assistive tech. A disabled control takes neither hover
735+
* nor focus, so the panel itself is the tooltip trigger and a tab stop
736+
* described by an `sr-only` copy of the reason. One trigger for the panel, not
737+
* one per control: the host's reason is the same for all of them. Draws
738+
* nothing of its own unless `readOnly` and the reason is non-blank, so a host
739+
* that passes no reason renders exactly as before.
740+
*/
741+
export function InspectorReadOnlyReason({ readOnly, reason: reasonProp, label, testId, children }: InspectorReadOnlyReasonProps) {
742+
const reasonId = React.useId();
743+
const reason = readOnly ? reasonProp?.trim() : undefined;
744+
if (!reason) return <>{children}</>;
745+
return (
746+
<TooltipProvider delayDuration={200}>
747+
<Tooltip>
748+
<TooltipTrigger asChild>
749+
<div
750+
role="group"
751+
aria-label={label}
752+
tabIndex={0}
753+
aria-describedby={reasonId}
754+
data-disabled-reason=""
755+
data-testid={testId}
756+
className="flex h-full min-h-0 flex-col focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
757+
>
758+
{children}
759+
<span id={reasonId} className="sr-only">{reason}</span>
760+
</div>
761+
</TooltipTrigger>
762+
<TooltipContent side="left" align="start">{reason}</TooltipContent>
763+
</Tooltip>
764+
</TooltipProvider>
765+
);
766+
}
767+
707768
/**
708769
* Helper to immutably splice an item in an array on a draft.
709770
* Returns a new array; never mutates input.

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.automationsReadOnly-11124.test.tsx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,26 @@ describe('Automations pillar on a read-only package (objectui#11124)', () => {
221221
expect(body).toHaveAccessibleDescription(READ_ONLY_REASON);
222222
});
223223

224+
it('a selected connection opens read-only, and its inspector carries the package reason (objectstack#29077)', async () => {
225+
renderPillar(true);
226+
await openFlow();
227+
228+
const hit = [...document.querySelectorAll('path > title')].find((el) => el.textContent === 'Start → End');
229+
expect(hit, 'the canvas must render the start → end connection').toBeTruthy();
230+
fireEvent.click(hit!.parentElement!);
231+
const rail = screen.getByRole('complementary');
232+
const remove = await within(rail).findByRole(
233+
'button',
234+
{ name: t('engine.inspector.flowEdge.remove', 'en') },
235+
{ timeout: 8000 },
236+
);
237+
expect(remove).toBeDisabled();
238+
const body = screen.getByTestId('flow-edge-readonly-reason');
239+
expect(body).toContainElement(remove);
240+
expect(body).toHaveAttribute('tabindex', '0');
241+
expect(body).toHaveAccessibleDescription(READ_ONLY_REASON);
242+
});
243+
224244
it('offers no canvas edit: no "add connected node" or "insert node" handle', async () => {
225245
renderPillar(true);
226246
await openFlow();

0 commit comments

Comments
 (0)