|
23 | 23 | * two dialogs open, and its pick carries the `description` remedy. So does the |
24 | 24 | * page `action:button` row, `ActionButtonPropsSchema` (#29844, ruling record |
25 | 25 | * 6597621758 letter A), for an ARRAY `params` only: an object `params` there is |
26 | | - * the static values and opens no dialog. The three doors share one predicate |
27 | | - * and one message (`./action-confirm-beside-params.ts`), pinned equal below. |
| 26 | + * the static values and opens no dialog. So do `action:icon` and the |
| 27 | + * `action:group` / `action:menu` members (#30017, ruling record 6624208841 |
| 28 | + * letter A), on the same terms. The doors share one predicate and one message |
| 29 | + * (`./action-confirm-beside-params.ts`), pinned equal below. |
28 | 30 | * |
29 | 31 | * The rejection tests assert the issue PATH and the MESSAGE SUBSTANCE rather |
30 | 32 | * than a bare `success === false`. One condition, one wording: a refusal whose |
|
35 | 37 | import { describe, expect, it } from 'vitest'; |
36 | 38 | import { ActionSchema, InlineActionSchema, defineAction } from './action.zod'; |
37 | 39 | import { BulkActionDefSchema } from './bulk-action.zod'; |
38 | | -import { ActionButtonPropsSchema } from './component.zod'; |
| 40 | +import { |
| 41 | + ActionButtonPropsSchema, |
| 42 | + ActionGroupPropsSchema, |
| 43 | + ActionIconPropsSchema, |
| 44 | + ActionMenuPropsSchema, |
| 45 | +} from './component.zod'; |
39 | 46 |
|
40 | 47 | /** |
41 | 48 | * Minimum legal registered action. `type` defaults to `script`, whose own |
@@ -378,3 +385,112 @@ describe('`confirmText` + non-empty ARRAY `params` is refused on the `action:but |
378 | 385 | expect(result.success, parseErrorOf(result)).toBe(true); |
379 | 386 | }); |
380 | 387 | }); |
| 388 | + |
| 389 | +describe('`confirmText` + non-empty ARRAY `params` is refused on `action:icon` and the `action:group` / `action:menu` members too', () => { |
| 390 | + // #30017 (ruling record 6624208841, letter A). `action:icon` forwards its |
| 391 | + // row's `confirmText` and an array `params` (as `actionParams`) to the same |
| 392 | + // `ActionRunner`, and so do both containers for each member: the same two |
| 393 | + // dialogs. Same predicate, same refusal: asserted against `action:button`'s |
| 394 | + // own issue, so a wording drifting onto one door goes red. An object `params` |
| 395 | + // (the icon's static values; a `type: 'api'` member's request payload) opens |
| 396 | + // no dialog, so it keeps `confirmText`. |
| 397 | + const pairParams = [{ name: 'reason', label: 'Reason', type: 'textarea' }]; |
| 398 | + const iconReject = { |
| 399 | + label: 'Reject', |
| 400 | + icon: 'x', |
| 401 | + actionType: 'api', |
| 402 | + target: '/api/v1/approvals/reject', |
| 403 | + params: pairParams, |
| 404 | + } as const; |
| 405 | + const memberReject = { |
| 406 | + name: 'reject', |
| 407 | + label: 'Reject', |
| 408 | + type: 'api', |
| 409 | + target: '/api/v1/approvals/reject', |
| 410 | + params: pairParams, |
| 411 | + } as const; |
| 412 | + const asContainer = (member: Record<string, unknown>) => ({ |
| 413 | + actions: [{ name: 'approve', label: 'Approve', type: 'script', target: 'approve' }, member], |
| 414 | + }); |
| 415 | + const CONTAINERS = [ |
| 416 | + ['action:group', ActionGroupPropsSchema], |
| 417 | + ['action:menu', ActionMenuPropsSchema], |
| 418 | + ] as const; |
| 419 | + |
| 420 | + const parseErrorOf = (result: { success: boolean; error?: unknown }) => JSON.stringify(result.error); |
| 421 | + /** `action:button`'s issue for the pair: the message every other door must equal. */ |
| 422 | + const buttonMessage = (): string | undefined => { |
| 423 | + const r = ActionButtonPropsSchema.safeParse({ ...iconReject, confirmText: 'Reject this request?' }); |
| 424 | + return r.success ? undefined : r.error.issues.find((i) => i.path.join('.') === 'confirmText')?.message; |
| 425 | + }; |
| 426 | + |
| 427 | + it('`action:icon` refuses the pair at the `confirmText` path, with `action:button`\'s message', () => { |
| 428 | + const result = ActionIconPropsSchema.safeParse({ ...iconReject, confirmText: 'Reject this request?' }); |
| 429 | + |
| 430 | + expect(result.success).toBe(false); |
| 431 | + const issues = result.success ? [] : result.error.issues; |
| 432 | + expect(issues).toHaveLength(1); |
| 433 | + expect(issues[0]?.code).toBe('custom'); |
| 434 | + expect(issues[0]?.path).toEqual(['confirmText']); |
| 435 | + expect(buttonMessage()).toBeDefined(); |
| 436 | + expect(issues[0]?.message).toBe(buttonMessage()); |
| 437 | + // The prescription: the remedy key, and the param-less case that keeps `confirmText` live. |
| 438 | + expect(issues[0]?.message).toContain('TWO dialogs'); |
| 439 | + expect(issues[0]?.message).toContain('`description`'); |
| 440 | + expect(issues[0]?.message).toContain('param-LESS'); |
| 441 | + }); |
| 442 | + |
| 443 | + for (const [row, schema] of CONTAINERS) { |
| 444 | + it(`an \`${row}\` member refuses the pair at \`actions.N.confirmText\`, with \`action:button\`'s message`, () => { |
| 445 | + const result = schema.safeParse(asContainer({ ...memberReject, confirmText: 'Reject this request?' })); |
| 446 | + |
| 447 | + expect(result.success).toBe(false); |
| 448 | + const issues = result.success ? [] : result.error.issues; |
| 449 | + expect(issues).toHaveLength(1); |
| 450 | + expect(issues[0]?.code).toBe('custom'); |
| 451 | + // The member's own index, so the editor underlines the offending member, not the list. |
| 452 | + expect(issues[0]?.path).toEqual(['actions', 1, 'confirmText']); |
| 453 | + expect(issues[0]?.message).toBe(buttonMessage()); |
| 454 | + expect(issues[0]?.message).toContain('TWO dialogs'); |
| 455 | + expect(issues[0]?.message).toContain('`description`'); |
| 456 | + }); |
| 457 | + |
| 458 | + it(`an \`${row}\` member refuses the localized-map form of \`confirmText\` too`, () => { |
| 459 | + const result = schema.safeParse(asContainer({ |
| 460 | + ...memberReject, |
| 461 | + confirmText: { en: 'Reject this request?', 'zh-CN': '拒绝该请求?' }, |
| 462 | + })); |
| 463 | + |
| 464 | + expect(result.success).toBe(false); |
| 465 | + expect(result.success ? [] : result.error.issues.map((i) => i.path.join('.'))).toEqual(['actions.1.confirmText']); |
| 466 | + }); |
| 467 | + } |
| 468 | + |
| 469 | + it('`action:icon` accepts `confirmText` with no `params`, beside `params: []`, and beside an OBJECT `params`', () => { |
| 470 | + const { params: _params, ...paramless } = iconReject; |
| 471 | + for (const props of [ |
| 472 | + { ...paramless, confirmText: 'Reject this request?' }, |
| 473 | + { ...iconReject, params: [], confirmText: 'Reject this request?' }, |
| 474 | + { ...iconReject, params: { reason: 'duplicate' }, confirmText: 'Reject this request?' }, |
| 475 | + { ...iconReject, description: 'Reject this request? Say why — the requester sees it.' }, |
| 476 | + ]) { |
| 477 | + const result = ActionIconPropsSchema.safeParse(props); |
| 478 | + expect(result.success, parseErrorOf(result)).toBe(true); |
| 479 | + } |
| 480 | + }); |
| 481 | + |
| 482 | + for (const [row, schema] of CONTAINERS) { |
| 483 | + it(`an \`${row}\` member accepts \`confirmText\` with no \`params\`, beside \`params: []\`, and beside a \`type: 'api'\` member's OBJECT \`params\``, () => { |
| 484 | + const { params: _params, ...paramless } = memberReject; |
| 485 | + for (const member of [ |
| 486 | + { ...paramless, confirmText: 'Reject this request?' }, |
| 487 | + { ...memberReject, params: [], confirmText: 'Reject this request?' }, |
| 488 | + { ...memberReject, params: { reason: 'duplicate' }, confirmText: 'Reject this request?' }, |
| 489 | + { ...memberReject, description: 'Reject this request? Say why — the requester sees it.' }, |
| 490 | + ]) { |
| 491 | + const result = schema.safeParse(asContainer(member)); |
| 492 | + expect(result.success, parseErrorOf(result)).toBe(true); |
| 493 | + } |
| 494 | + }); |
| 495 | + } |
| 496 | +}); |
0 commit comments