Skip to content

Commit d4427ae

Browse files
test(components): pin a declared danger action on action:bar to the app-todo shape (#27300)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8UhZd4buJ9P8RHCa2Rnbx4C)_ Fixes #26781 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只新增一个测试文件,用 app-todo 的 `delete_completed`(危险操作)原样形状驱动 `action:bar`,钉住按钮画成红色(destructive)、确认框按钮也是红色。 - 为什么改:代码修复已随 #26966 合入,但没有任何测试经过 `action:bar` 走这条路径,卡要求「在 app-todo 形状上钉住」。 - 风险与回滚:零运行时风险,只加测试;回滚即删这一个文件。 - 先看:`ui/packages/components/src/renderers/action/__tests__/action-bar-danger-variant-26781.test.tsx`(三个用例:危险按钮样式、对照按钮、确认框选项)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/components/src/renderers/action/__tests__/action-bar-danger-variant-26781.test.tsx`,预期 3 passed。 - 需要你做的:无,席位落地。 ## What changes One new test file, `action-bar-danger-variant-26781.test.tsx`, under `ui/packages/components/src/renderers/action/__tests__/`. No source change. It renders an `action:bar` shaped like the list page's toolbar (`objectViewHeaderToolbar.ts`: `location: 'list_toolbar'`, `size: 'sm'`, `variant: 'outline'`) carrying two actions copied key for key from `examples/app-todo` (`DeleteCompletedAction` and `ExportToCsvAction`, `ai` block included), and asserts: 1. the `delete_completed` member (`variant: 'danger'`) draws with the Button primitive's destructive style (`bg-destructive`, not the bar's outline); 2. CONTROL: `export_csv` in the same bar declares no variant and draws the bar's `outline`; 3. clicking `delete_completed` asks its `confirmText` through the confirm handler with `{ title, confirmText: label, destructive: true }`, and the script handler is not run on a cancel. ## Why The code path landed with #26966 (`toButtonVariant` maps `danger` to the primitive's `destructive`; `toRunnerVariant` forwards `danger` to the runner, whose confirm paints destructive). The existing `danger` pins (`action-variant-confirm-26665.test.tsx`) drive `action:button` / icon / group / menu directly; nothing drove a declared `danger` action through `action:bar`, which is where a list toolbar action reaches the screen. The card's Wanted asks for that pin on the app-todo shape. ## Tests - New file: 3 passed (vitest, `dom` project), run from the `ui/` root. - Ablation A (one-shot, restored): `toButtonVariant` `case 'danger'` returns `default` ⇒ test 1 red (`expected '…' to contain 'bg-destructive'`), 2 passed; restore verified (blob equal to HEAD, `git diff HEAD` empty). - Ablation B (one-shot, restored): `toRunnerVariant` drops its `danger` case ⇒ test 3 red (options lose `destructive`), 2 passed; restore verified the same way. - `pnpm --filter @object-ui/components run type-check` exit 0 (its `tsconfig.test.json` includes `src/**/*.test.tsx`). ## Acceptance notes - The confirm is read from the confirm handler's call, the input the host's `ActionConfirmDialog` (`@object-ui/app-shell`) draws from. `@object-ui/components` cannot import app-shell, so the dialog's own button class is not rendered here; this is the same layer the 26665 pins read. ## Verification <details><summary>Derived gates — 24 derived, 24 run, all exit 0, on head 0d2d769</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 24 commands; each was run with its exit captured before any pipe; `--ran` reconciliation: "24 derived famil(ies) accounted for — 24 run, 0 NOT-MEASURED". `--tier`: no path-derived mandate; changed lines 143, under the 3000 threshold. Extra: eslint on the file exit 0; `pnpm --dir ui run check:new-line-citations` reports 0 new citations. </details> Tests only, nothing a released package ships: `skip-changeset`. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbXi3XdpqE7y1mW7EZt)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a0aa8a1 commit d4427ae

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#26781 — a declared `danger` action on `action:bar` draws with
11+
* the destructive button style and confirms through the destructive button.
12+
*
13+
* The producer is `examples/app-todo`'s `DeleteCompletedAction`
14+
* (`delete_completed`: `locations: ['list_toolbar']`, `variant: 'danger'`,
15+
* `confirmText`). The list page hands an object's actions to `action:bar` as
16+
* they are served, and `action:bar` spreads each one onto an `action:button`
17+
* member with `variant: action.variant || schema.variant`. Before
18+
* objectstack#26779 the Button primitive had no `danger` variant, so that
19+
* member drew with no variant style and its confirm stayed neutral; the code
20+
* path is fixed by `toButtonVariant` / `toRunnerVariant` (`../runner-variant`).
21+
* `action-variant-confirm-26665.test.tsx` drives `action:button` / icon /
22+
* group / menu directly; this file drives the declared action through the bar.
23+
*
24+
* The two fixtures copy the app-todo literals key for key, `ai` block
25+
* included, because that is the shape the served metadata carries. The bar
26+
* copies `objectViewHeaderToolbar.ts`'s list toolbar (`location`, `size`,
27+
* `variant: 'outline'`), so the bar's own variant is in play. The control,
28+
* app-todo's `export_csv` from the same toolbar, declares no variant and draws
29+
* the bar's `outline`, which tells "the member drew its own variant" apart from
30+
* "every member draws destructive".
31+
*
32+
* The confirm is read from the confirm handler's own call, the artefact the
33+
* host's dialog (`ActionConfirmDialog` in `@object-ui/app-shell`) is drawn
34+
* from: `destructive: true` is what paints its confirm button destructive.
35+
*/
36+
37+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
38+
import { render, screen, waitFor, fireEvent, within } from '@testing-library/react';
39+
import React from 'react';
40+
import { ComponentRegistry } from '@object-ui/core';
41+
import type { ActionContext, ActionDef, ActionResult, ConfirmationHandler } from '@object-ui/core';
42+
import { ActionProvider } from '@object-ui/react';
43+
// Module-scope side-effect imports: `action:bar` resolves its members and its
44+
// overflow menu through the ComponentRegistry at render time, and the light
45+
// `dom` project does not load the `@object-ui/components` graph. Module scope,
46+
// not a `beforeAll`, per AGENTS.md §测试纪律.
47+
import '../action-bar';
48+
import '../action-button';
49+
import '../action-menu';
50+
51+
/** `examples/app-todo` `DeleteCompletedAction`, as declared. */
52+
const DELETE_COMPLETED = {
53+
name: 'delete_completed',
54+
label: 'Delete Completed',
55+
objectName: 'todo_task',
56+
icon: 'trash',
57+
type: 'script',
58+
target: 'deleteCompletedTasks',
59+
locations: ['list_toolbar'],
60+
variant: 'danger',
61+
confirmText: 'Permanently delete all completed tasks? This cannot be undone.',
62+
successMessage: 'Completed tasks deleted!',
63+
refreshAfter: true,
64+
ai: {
65+
exposed: true,
66+
description:
67+
'Permanently delete every completed todo task. Destructive and irreversible — only after the user confirms.',
68+
},
69+
};
70+
71+
/** `examples/app-todo` `ExportToCsvAction`, as declared: the same toolbar, no variant. */
72+
const EXPORT_CSV = {
73+
name: 'export_csv',
74+
label: 'Export to CSV',
75+
objectName: 'todo_task',
76+
icon: 'download',
77+
type: 'script',
78+
target: 'exportTasksToCSV',
79+
locations: ['list_toolbar'],
80+
successMessage: 'Export completed!',
81+
refreshAfter: false,
82+
ai: {
83+
exposed: true,
84+
description: 'Export the current list of todo tasks to a downloadable CSV file.',
85+
},
86+
};
87+
88+
let script: Mock<(action: ActionDef, ctx: ActionContext) => Promise<ActionResult>>;
89+
let confirm: Mock<ConfirmationHandler>;
90+
91+
beforeEach(() => {
92+
script = vi.fn(async () => ({ success: true }));
93+
// A cancel: the question is what is measured, not the run.
94+
confirm = vi.fn(async () => false);
95+
});
96+
97+
function renderToolbar() {
98+
const Bar = ComponentRegistry.get('action:bar');
99+
if (!Bar) throw new Error('action:bar is not registered');
100+
const schema = {
101+
type: 'action:bar',
102+
location: 'list_toolbar',
103+
actions: [DELETE_COMPLETED, EXPORT_CSV],
104+
size: 'sm',
105+
variant: 'outline',
106+
};
107+
render(
108+
<ActionProvider handlers={{ script }} onConfirm={confirm}>
109+
<Bar schema={schema as never} />
110+
</ActionProvider>,
111+
);
112+
return screen.getByRole('toolbar');
113+
}
114+
115+
describe('action:bar draws and confirms a declared `danger` action destructive (objectstack#26781)', () => {
116+
it('the app-todo `delete_completed` member draws with the destructive button style', () => {
117+
const toolbar = renderToolbar();
118+
const del = within(toolbar).getByRole('button', { name: 'Delete Completed' });
119+
expect(del.className).toContain('bg-destructive');
120+
expect(del.className).not.toContain('border-input');
121+
});
122+
123+
it('CONTROL: `export_csv` in the same bar declares no variant and draws the bar\'s `outline`', () => {
124+
const toolbar = renderToolbar();
125+
const csv = within(toolbar).getByRole('button', { name: 'Export to CSV' });
126+
expect(csv.className).toContain('border-input');
127+
expect(csv.className).not.toContain('bg-destructive');
128+
});
129+
130+
it('clicking `delete_completed` asks its confirmText through the destructive confirm button', async () => {
131+
const toolbar = renderToolbar();
132+
fireEvent.click(within(toolbar).getByRole('button', { name: 'Delete Completed' }));
133+
await waitFor(() => expect(confirm).toHaveBeenCalledTimes(1));
134+
expect(script).not.toHaveBeenCalled();
135+
const [message, options] = confirm.mock.calls[0];
136+
expect(message).toBe(DELETE_COMPLETED.confirmText);
137+
expect(options).toEqual({
138+
title: 'Delete Completed',
139+
confirmText: 'Delete Completed',
140+
destructive: true,
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)