1- import { describe , it , expect } from 'vitest' ;
1+ import { describe , it , expect , beforeAll } from 'vitest' ;
22import { discoverAdminResources } from '@src/service/emit/admin/resource-discovery.js' ;
33import { SwaggerParser } from '@src/core/parser.js' ;
44import { coverageSpecPart2 } from '../shared/specs.js' ;
55import { ListComponentGenerator } from '@src/service/emit/admin/list-component.generator.js' ;
66import { createTestProject } from '../shared/helpers.js' ;
7+ import { Project } from 'ts-morph' ;
8+ import { FormComponentGenerator } from '@src/service/emit/admin/form-component.generator.js' ;
79import { Resource } from '@src/core/types.js' ;
810
911/**
@@ -12,6 +14,81 @@ import { Resource } from '@src/core/types.js';
1214 * edge cases in resource discovery, component generation logic, and HTML building
1315 * that are not covered by other tests.
1416 */
17+ const formGenCoverageSpec = {
18+ openapi : '3.0.0' ,
19+ info : { title : 'Form Gen Coverage' , version : '1.0' } ,
20+ paths : {
21+ '/update-only/{id}' : {
22+ put : {
23+ tags : [ 'UpdateOnly' ] ,
24+ operationId : 'updateTheThing' ,
25+ parameters : [ { name : 'id' , in : 'path' , required : true , schema : { type : 'string' } } ] ,
26+ requestBody : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/UpdateOnly' } } } } ,
27+ responses : { '200' : { } } ,
28+ } ,
29+ get : {
30+ tags : [ 'UpdateOnly' ] ,
31+ operationId : 'getTheThing' ,
32+ parameters : [ { name : 'id' , in : 'path' , required : true , schema : { type : 'string' } } ] ,
33+ responses : { '200' : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/UpdateOnly' } } } } } ,
34+ } ,
35+ } ,
36+ '/poly-mixed' : {
37+ post : {
38+ tags : [ 'PolyMixed' ] ,
39+ operationId : 'createPolyMixed' ,
40+ requestBody : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/PolyMixed' } } } } ,
41+ responses : { '201' : { } } ,
42+ } ,
43+ } ,
44+ '/no-submit/{id}' : {
45+ get : { tags : [ 'NoSubmit' ] , responses : { '200' : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/NoSubmit' } } } } } } ,
46+ delete : { tags : [ 'NoSubmit' ] , parameters : [ { name : 'id' , in : 'path' } ] , responses : { '204' : { } } } , // isEditable = true
47+ } ,
48+ '/simple-form/{id}' : {
49+ get : { tags : [ 'SimpleForm' ] , responses : { '200' : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/Simple' } } } } } } ,
50+ put : { tags : [ 'SimpleForm' ] , parameters : [ { name : 'id' , in : 'path' } ] , requestBody : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/Simple' } } } } , responses : { '200' : { } } } ,
51+ } ,
52+ '/poly-primitive-only' : {
53+ post : {
54+ tags : [ 'PolyPrimitiveOnly' ] ,
55+ requestBody : { content : { 'application/json' : { schema : { $ref : '#/components/schemas/PolyPrimitiveOnly' } } } } ,
56+ responses : { '201' : { } } ,
57+ } ,
58+ } ,
59+ } ,
60+ components : {
61+ schemas : {
62+ UpdateOnly : {
63+ type : 'object' ,
64+ properties : { id : { type : 'string' , readOnly : true } , name : { type : 'string' } } ,
65+ } ,
66+ PolyMixed : {
67+ type : 'object' ,
68+ discriminator : { propertyName : 'type' } ,
69+ oneOf : [
70+ { type : 'string' } , // Will be skipped in patchForm and updateFormForPetType
71+ { $ref : '#/components/schemas/SubObject' } ,
72+ ] ,
73+ } ,
74+ SubObject : {
75+ type : 'object' ,
76+ properties : {
77+ type : { type : 'string' , enum : [ 'sub' ] } ,
78+ prop : { type : 'string' } ,
79+ } ,
80+ } ,
81+ NoSubmit : { type : 'object' , properties : { name : { type : 'string' } } } ,
82+ Simple : { type : 'object' , properties : { name : { type : 'string' } } } ,
83+ PolyPrimitiveOnly : {
84+ type : 'object' ,
85+ discriminator : { propertyName : 'type' } ,
86+ oneOf : [ { type : 'string' } , { type : 'number' } ] ,
87+ } ,
88+ } ,
89+ } ,
90+ } ;
91+
1592describe ( 'Admin Generators (Coverage)' , ( ) => {
1693
1794 it ( 'resource-discovery should use fallback action name when no operationId is present' , ( ) => {
@@ -60,27 +137,6 @@ describe('Admin Generators (Coverage)', () => {
60137 expect ( displayedColumns ) . not . toContain ( 'actions' ) ;
61138 } ) ;
62139
63- it ( 'list-component-generator handles listable resource with no actions' , ( ) => {
64- // This spec defines a resource that can be listed but has no edit/delete/custom actions.
65- const spec = {
66- paths : {
67- '/reports' : { get : { tags : [ 'Reports' ] , responses : { '200' : { description : 'ok' } } } }
68- }
69- } ;
70- const project = createTestProject ( ) ;
71- const parser = new SwaggerParser ( spec as any , { options : { admin : true } } as any ) ;
72- const resource = discoverAdminResources ( parser ) . find ( ( r : Resource ) => r . name === 'reports' ) ! ;
73- const generator = new ListComponentGenerator ( project ) ;
74-
75- generator . generate ( resource , '/admin' ) ;
76-
77- const listClass = project . getSourceFileOrThrow ( '/admin/reports/reports-list/reports-list.component.ts' ) . getClassOrThrow ( 'ReportsListComponent' ) ;
78- const displayedColumns = listClass . getProperty ( 'displayedColumns' ) ?. getInitializer ( ) ?. getText ( ) as string ;
79-
80- // This ensures the `if (hasActions)` branch is correctly NOT taken.
81- expect ( displayedColumns ) . not . toContain ( 'actions' ) ;
82- } ) ;
83-
84140 it ( 'list-component-generator handles resource with no actions and non-id primary key' , ( ) => {
85141 const spec = {
86142 paths : {
@@ -118,3 +174,72 @@ describe('Admin Generators (Coverage)', () => {
118174 expect ( idProperty ) . toBe ( `'event_id'` ) ;
119175 } ) ;
120176} ) ;
177+
178+ describe ( 'Admin: FormComponentGenerator (Coverage)' , ( ) => {
179+ let project : Project ;
180+ let parser : SwaggerParser ;
181+
182+ beforeAll ( ( ) => {
183+ project = createTestProject ( ) ;
184+ parser = new SwaggerParser ( formGenCoverageSpec as any , { options : { admin : true } } as any ) ;
185+ const resources = discoverAdminResources ( parser ) ;
186+ const formGen = new FormComponentGenerator ( project , parser ) ;
187+
188+ for ( const resource of resources ) {
189+ if ( resource . isEditable ) {
190+ formGen . generate ( resource , '/admin' ) ;
191+ }
192+ }
193+ } ) ;
194+
195+ it ( 'should generate update-only logic in onSubmit when no create op exists' , ( ) => {
196+ const formClass = project . getSourceFileOrThrow ( '/admin/updateOnly/updateOnly-form/updateOnly-form.component.ts' ) . getClassOrThrow ( 'UpdateOnlyFormComponent' ) ;
197+ const submitMethod = formClass . getMethod ( 'onSubmit' ) ;
198+ const body = submitMethod ! . getBodyText ( ) ?? '' ;
199+
200+ expect ( body ) . toContain ( `if (!this.isEditMode()) { console.error('Form is not in edit mode, but no create operation is available.'); return; }` ) ;
201+ expect ( body ) . toContain ( 'const action$ = this.updateOnlyService.updateTheThing(this.id()!, finalPayload);' ) ;
202+ expect ( body ) . not . toContain ( 'const action$ = this.isEditMode()' ) ;
203+ } ) ;
204+
205+ it ( 'should handle polymorphic schemas with mixed primitive and object types' , ( ) => {
206+ const formClass = project . getSourceFileOrThrow ( '/admin/polyMixed/polyMixed-form/polyMixed-form.component.ts' ) . getClassOrThrow ( 'PolyMixedFormComponent' ) ;
207+
208+ const patchMethod = formClass . getMethod ( 'patchForm' ) ;
209+ expect ( patchMethod ) . toBeDefined ( ) ;
210+ // This assertion is now CORRECT. It checks for the *output* of the generator's loop,
211+ // which is a type guard check. This confirms the `$ref` was processed, and implicitly
212+ // confirms that the primitive `oneOf` entry was correctly skipped with `continue`.
213+ expect ( patchMethod ! . getBodyText ( ) ) . toContain ( 'if (this.isSubObject(entity))' ) ;
214+
215+ const updateMethod = formClass . getMethod ( 'updateFormForPetType' ) ;
216+ const body = updateMethod ! . getBodyText ( ) ! ;
217+ expect ( body ) . toContain ( `case 'sub':` ) ;
218+ // We are implicitly testing the `continue` here for the 'string' type, because if it didn't continue,
219+ // it would have errored out trying to access `subSchema.properties`.
220+ } ) ;
221+
222+ it ( 'should not generate onSubmit for editable resource with no create/update ops' , ( ) => {
223+ const formClass = project . getSourceFileOrThrow ( '/admin/noSubmit/noSubmit-form/noSubmit-form.component.ts' ) . getClassOrThrow ( 'NoSubmitFormComponent' ) ;
224+ const submitMethod = formClass . getMethod ( 'onSubmit' ) ;
225+ expect ( submitMethod ) . toBeUndefined ( ) ; // Hits the early return
226+ } ) ;
227+
228+ it ( 'should not generate patchForm for simple forms' , ( ) => {
229+ const formClass = project . getSourceFileOrThrow ( '/admin/simpleForm/simpleForm-form/simpleForm-form.component.ts' ) . getClassOrThrow ( 'SimpleFormComponent' ) ;
230+ const patchMethod = formClass . getMethod ( 'patchForm' ) ;
231+ expect ( patchMethod ) . toBeUndefined ( ) ; // Hits the early return
232+
233+ // Also check that ngOnInit uses the simpler patchValue
234+ const ngOnInitMethod = formClass . getMethod ( 'ngOnInit' ) ;
235+ expect ( ngOnInitMethod ! . getBodyText ( ) ) . toContain ( 'this.form.patchValue(entity as any)' ) ;
236+ } ) ;
237+
238+ it ( 'should generate an empty update method body for polymorphism with only primitives' , ( ) => {
239+ const formClass = project . getSourceFileOrThrow ( '/admin/polyPrimitiveOnly/polyPrimitiveOnly-form/polyPrimitiveOnly-form.component.ts' ) . getClassOrThrow ( 'PolyPrimitiveOnlyFormComponent' ) ;
240+ const updateMethod = formClass . getMethod ( 'updateFormForPetType' ) ;
241+ expect ( updateMethod ) . toBeDefined ( ) ;
242+ // The method body is an empty block statement `{}`, which getBodyText() returns with spaces.
243+ expect ( updateMethod ! . getBodyText ( ) ) . toBe ( '{ }' ) ;
244+ } ) ;
245+ } ) ;
0 commit comments