|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * #27864 — the New Permission Set form asks for the Label first, and requires it. |
| 5 | + * |
| 6 | + * The form opened on Name (the API name) with an optional Label below it, so an |
| 7 | + * administrator typed a machine name first and could save a set with no human |
| 8 | + * label, while the Console's create form derives `name` from the label. The |
| 9 | + * Identity section now lists `label` first and required, with `name` after it. |
| 10 | + * |
| 11 | + * `required` on a form row is a layout flag the renderer reads: the set's |
| 12 | + * schema still accepts a set with no label, which the last case pins so that a |
| 13 | + * later move of the accept set is a deliberate edit, not a form side effect. |
| 14 | + */ |
| 15 | + |
| 16 | +import { describe, expect, it } from 'vitest'; |
| 17 | + |
| 18 | +import { METADATA_FORM_REGISTRY } from '../system/metadata-form-registry'; |
| 19 | +import { permissionForm } from './permission.form'; |
| 20 | +import { PermissionSetSchema } from './permission.zod'; |
| 21 | + |
| 22 | +type Row = { field: string; required?: boolean; helpText?: string }; |
| 23 | + |
| 24 | +const sections = (permissionForm.sections ?? []) as Array<{ label?: string; fields?: Array<Row | string> }>; |
| 25 | +const identity = sections.find((s) => s.label === 'Identity'); |
| 26 | +const rows = (identity?.fields ?? []).map((f) => (typeof f === 'string' ? { field: f } : f)) as Row[]; |
| 27 | +const row = (key: string) => rows.find((r) => r.field === key); |
| 28 | + |
| 29 | +describe('permission-set form — Label first and required, Name after it', () => { |
| 30 | + it('is the form `/meta/types` serves for the permission kind', () => { |
| 31 | + expect(METADATA_FORM_REGISTRY.permission).toBe(permissionForm); |
| 32 | + }); |
| 33 | + |
| 34 | + it('opens on the Identity section, whose first two rows are label then name', () => { |
| 35 | + expect(sections[0]?.label).toBe('Identity'); |
| 36 | + expect(rows.slice(0, 2).map((r) => r.field)).toEqual(['label', 'name']); |
| 37 | + }); |
| 38 | + |
| 39 | + it('lists label before name across the whole form', () => { |
| 40 | + const order = sections.flatMap((s) => (s.fields ?? []).map((f) => (typeof f === 'string' ? f : f.field))); |
| 41 | + expect(order.indexOf('label')).toBeGreaterThanOrEqual(0); |
| 42 | + expect(order.indexOf('label')).toBeLessThan(order.indexOf('name')); |
| 43 | + }); |
| 44 | + |
| 45 | + it('requires the label, and still requires the name', () => { |
| 46 | + expect(row('label')?.required).toBe(true); |
| 47 | + expect(row('name')?.required).toBe(true); |
| 48 | + // CONTROL: a neighbouring optional row is not swept into `required`. |
| 49 | + expect(row('description')?.required).toBeUndefined(); |
| 50 | + }); |
| 51 | + |
| 52 | + it("the name's help says it comes from the label", () => { |
| 53 | + expect(row('name')?.helpText).toMatch(/derived from the label/i); |
| 54 | + expect(row('name')?.helpText).toMatch(/snake_case/); |
| 55 | + }); |
| 56 | + |
| 57 | + it('moves no accept set: the schema still parses a set with no label', () => { |
| 58 | + const r = PermissionSetSchema.safeParse({ name: 'crm_sales_rep', objects: {} }); |
| 59 | + expect(r.success).toBe(true); |
| 60 | + // CONTROL: the same parse with a label is also clean. |
| 61 | + expect(PermissionSetSchema.safeParse({ name: 'crm_sales_rep', label: 'Sales Rep', objects: {} }).success).toBe(true); |
| 62 | + }); |
| 63 | +}); |
0 commit comments