@@ -29,30 +29,54 @@ const roleOptionsOf = (object: unknown): { label: string; value: string }[] =>
2929describe ( '#3723 normalizeAdditionalOrgRoles — the one normalizer' , ( ) => {
3030 it ( 'keeps valid app role names, in declaration order' , ( ) => {
3131 expect ( normalizeAdditionalOrgRoles ( [ 'sales_rep' , 'sales_manager' , 'service_agent' ] ) ) . toEqual ( [
32- 'sales_rep' ,
33- 'sales_manager' ,
34- 'service_agent' ,
32+ { name : 'sales_rep' } ,
33+ { name : 'sales_manager' } ,
34+ { name : 'service_agent' } ,
3535 ] ) ;
3636 } ) ;
3737
38+ it ( 'carries a declared label through, and tolerates entries without one' , ( ) => {
39+ expect (
40+ normalizeAdditionalOrgRoles ( [ { name : 'sales_rep' , label : '销售代表' } , 'ops' ] ) ,
41+ ) . toEqual ( [ { name : 'sales_rep' , label : '销售代表' } , { name : 'ops' } ] ) ;
42+ } ) ;
43+
44+ it ( 'drops a blank label rather than storing an empty picker entry' , ( ) => {
45+ expect ( normalizeAdditionalOrgRoles ( [ { name : 'ops' , label : ' ' } ] ) ) . toEqual ( [ { name : 'ops' } ] ) ;
46+ } ) ;
47+
3848 it ( 'drops the built-ins — an app may not redefine owner/admin/member/delegated_admin' , ( ) => {
3949 // Silently downgrading `owner` to a plain-member role would strip its
4050 // `invitation:create` and 403 every org mutation.
4151 expect (
4252 normalizeAdditionalOrgRoles ( [ 'owner' , 'admin' , 'member' , 'delegated_admin' , 'sales_rep' ] ) ,
43- ) . toEqual ( [ 'sales_rep' ] ) ;
53+ ) . toEqual ( [ { name : 'sales_rep' } ] ) ;
4454 } ) ;
4555
4656 it ( 'de-duplicates and trims' , ( ) => {
47- expect ( normalizeAdditionalOrgRoles ( [ ' sales_rep ' , 'sales_rep' , '' ] ) ) . toEqual ( [ 'sales_rep' ] ) ;
57+ expect ( normalizeAdditionalOrgRoles ( [ ' sales_rep ' , 'sales_rep' , '' ] ) ) . toEqual ( [
58+ { name : 'sales_rep' } ,
59+ ] ) ;
4860 } ) ;
4961
50- it ( 'ignores non-strings and a non-array input' , ( ) => {
51- expect ( normalizeAdditionalOrgRoles ( [ null , 42 , { } , 'sales_rep' ] as unknown [ ] ) ) . toEqual ( [ 'sales_rep' ] ) ;
62+ it ( 'ignores unusable entries and a non-array input' , ( ) => {
63+ expect ( normalizeAdditionalOrgRoles ( [ null , 42 , { } , { name : 7 } , 'sales_rep' ] as unknown [ ] ) ) . toEqual ( [
64+ { name : 'sales_rep' } ,
65+ ] ) ;
5266 expect ( normalizeAdditionalOrgRoles ( undefined ) ) . toEqual ( [ ] ) ;
5367 expect ( normalizeAdditionalOrgRoles ( null ) ) . toEqual ( [ ] ) ;
5468 } ) ;
5569
70+ it ( 'a blank label never masks the built-in drop or the name filter' , ( ) => {
71+ expect (
72+ normalizeAdditionalOrgRoles ( [
73+ { name : 'owner' , label : 'Boss' } ,
74+ { name : 'bad.name' , label : 'Bad' } ,
75+ { name : 'ok_role' , label : 'OK' } ,
76+ ] ) ,
77+ ) . toEqual ( [ { name : 'ok_role' , label : 'OK' } ] ) ;
78+ } ) ;
79+
5680 it ( 'REFUSES a name that cannot round-trip through Field.select — loudly' , ( ) => {
5781 // `Field.select` lowercases values and strips everything outside
5882 // [a-z0-9_], so `showcase.export_data` would be registered with
@@ -64,7 +88,7 @@ describe('#3723 normalizeAdditionalOrgRoles — the one normalizer', () => {
6488 [ 'showcase.export_data' , 'Sales Rep' , '_leading' , 'x' , 'sales_rep' ] ,
6589 { warn } ,
6690 ) ,
67- ) . toEqual ( [ 'sales_rep' ] ) ;
91+ ) . toEqual ( [ { name : 'sales_rep' } ] ) ;
6892 expect ( warn ) . toHaveBeenCalledTimes ( 4 ) ;
6993 expect ( warn . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( '\n' ) ) . toContain ( 'showcase.export_data' ) ;
7094 } ) ;
@@ -81,13 +105,13 @@ describe('#3723 membershipRoleOptions — the option list both selects get', ()
81105 } ) ;
82106
83107 it ( 'appends app roles after the built-ins, with humanized labels' , ( ) => {
84- const options = membershipRoleOptions ( [ 'sales_rep' ] ) ;
108+ const options = membershipRoleOptions ( [ { name : 'sales_rep' } ] ) ;
85109 expect ( values ( options ) ) . toEqual ( [ 'owner' , 'admin' , 'delegated_admin' , 'member' , 'sales_rep' ] ) ;
86110 expect ( options . at ( - 1 ) ) . toEqual ( { label : 'Sales Rep' , value : 'sales_rep' } ) ;
87111 } ) ;
88112
89113 it ( 'never duplicates a built-in' , ( ) => {
90- expect ( values ( membershipRoleOptions ( [ 'member' , 'sales_rep' ] ) ) ) . toEqual ( [
114+ expect ( values ( membershipRoleOptions ( [ { name : 'member' } , { name : 'sales_rep' } ] ) ) ) . toEqual ( [
91115 'owner' ,
92116 'admin' ,
93117 'delegated_admin' ,
@@ -99,11 +123,20 @@ describe('#3723 membershipRoleOptions — the option list both selects get', ()
99123 it ( 'humanizes labels without ever changing the stored value' , ( ) => {
100124 expect ( membershipRoleLabel ( 'sales_rep' ) ) . toBe ( 'Sales Rep' ) ;
101125 expect ( membershipRoleLabel ( 'ops' ) ) . toBe ( 'Ops' ) ;
102- expect ( membershipRoleOptions ( [ 'field_ops_delegate' ] ) . at ( - 1 ) ) . toEqual ( {
126+ expect ( membershipRoleOptions ( [ { name : 'field_ops_delegate' } ] ) . at ( - 1 ) ) . toEqual ( {
103127 label : 'Field Ops Delegate' ,
104128 value : 'field_ops_delegate' ,
105129 } ) ;
106130 } ) ;
131+
132+ it ( 'a declared label WINS over the title-cased machine name' , ( ) => {
133+ // The whole point: a position that says `销售代表` must not be rendered as
134+ // "Sales Rep" by a picker deriving its own label from the machine name.
135+ expect ( membershipRoleOptions ( [ { name : 'sales_rep' , label : '销售代表' } ] ) . at ( - 1 ) ) . toEqual ( {
136+ label : '销售代表' ,
137+ value : 'sales_rep' ,
138+ } ) ;
139+ } ) ;
107140} ) ;
108141
109142describe ( '#3723 withMembershipRoleOptions — materializing onto the platform objects' , ( ) => {
@@ -112,27 +145,27 @@ describe('#3723 withMembershipRoleOptions — materializing onto the platform ob
112145 it ( 'widens BOTH role selects — fixing one leaves the other rejecting the same value' , ( ) => {
113146 // The lesson from #3722: `sys_member.role` alone still left
114147 // `sys_invitation.role` refusing the invitation at issuance.
115- const [ , member , invitation ] = withMembershipRoleOptions ( objects , [ 'sales_rep' ] ) ;
148+ const [ , member , invitation ] = withMembershipRoleOptions ( objects , [ { name : 'sales_rep' } ] ) ;
116149 expect ( values ( roleOptionsOf ( member ) ) ) . toContain ( 'sales_rep' ) ;
117150 expect ( values ( roleOptionsOf ( invitation ) ) ) . toContain ( 'sales_rep' ) ;
118151 expect ( values ( roleOptionsOf ( member ) ) ) . toEqual ( values ( roleOptionsOf ( invitation ) ) ) ;
119152 } ) ;
120153
121154 it ( 'leaves unrelated objects untouched, by identity' , ( ) => {
122- const [ user ] = withMembershipRoleOptions ( objects , [ 'sales_rep' ] ) ;
155+ const [ user ] = withMembershipRoleOptions ( objects , [ { name : 'sales_rep' } ] ) ;
123156 expect ( user ) . toBe ( SysUser ) ;
124157 } ) ;
125158
126159 it ( 'is copy-on-write — the shared module-level definitions are never mutated' , ( ) => {
127160 // `authIdentityObjects` is a process-wide singleton also used by the
128161 // compile-time `objectstack.config.ts`; two kernels booted with different
129162 // app roles in one test run must not see each other's options.
130- withMembershipRoleOptions ( objects , [ 'sales_rep' ] ) ;
163+ withMembershipRoleOptions ( objects , [ { name : 'sales_rep' } ] ) ;
131164 expect ( values ( roleOptionsOf ( SysMember ) ) ) . toEqual ( values ( BUILTIN_MEMBERSHIP_ROLE_OPTIONS ) ) ;
132165 expect ( values ( roleOptionsOf ( SysInvitation ) ) ) . toEqual ( values ( BUILTIN_MEMBERSHIP_ROLE_OPTIONS ) ) ;
133166
134- const a = withMembershipRoleOptions ( objects , [ 'role_a' ] ) ;
135- const b = withMembershipRoleOptions ( objects , [ 'role_b' ] ) ;
167+ const a = withMembershipRoleOptions ( objects , [ { name : 'role_a' } ] ) ;
168+ const b = withMembershipRoleOptions ( objects , [ { name : 'role_b' } ] ) ;
136169 expect ( values ( roleOptionsOf ( a [ 1 ] ) ) ) . toContain ( 'role_a' ) ;
137170 expect ( values ( roleOptionsOf ( a [ 1 ] ) ) ) . not . toContain ( 'role_b' ) ;
138171 expect ( values ( roleOptionsOf ( b [ 1 ] ) ) ) . toContain ( 'role_b' ) ;
@@ -158,20 +191,25 @@ describe('#3723 collectStackOrgRoles — one walk for every host', () => {
158191 positions : [ { name : 'sales_rep' } , { name : 'sales_manager' } ] ,
159192 permissions : [ { name : 'sales_user' } , { name : 'guest_portal' } ] ,
160193 } ) ;
161- expect ( roles ) . toEqual ( [ 'sales_rep' , 'sales_manager' , 'sales_user' , 'guest_portal' ] ) ;
194+ expect ( roles . map ( ( r ) => r . name ) ) . toEqual ( [
195+ 'sales_rep' ,
196+ 'sales_manager' ,
197+ 'sales_user' ,
198+ 'guest_portal' ,
199+ ] ) ;
162200 } ) ;
163201
164202 it ( 'accepts bare strings as well as named entries' , ( ) => {
165203 expect ( collectStackOrgRoles ( { positions : [ 'sales_rep' , { name : 'ops' } ] } ) ) . toEqual ( [
166- 'sales_rep' ,
167- 'ops' ,
204+ { name : 'sales_rep' } ,
205+ { name : 'ops' } ,
168206 ] ) ;
169207 } ) ;
170208
171209 it ( 'normalizes on the way out — the built-ins never leak into the extras' , ( ) => {
172210 expect (
173211 collectStackOrgRoles ( { positions : [ { name : 'member' } , { name : 'owner' } , { name : 'ops' } ] } ) ,
174- ) . toEqual ( [ 'ops' ] ) ;
212+ ) . toEqual ( [ { name : 'ops' } ] ) ;
175213 } ) ;
176214
177215 it ( 'a stack with no role metadata yields nothing (and does not throw)' , ( ) => {
0 commit comments