@@ -20,6 +20,7 @@ import {
20
20
ComponentMetadata ,
21
21
ConcatenatedStudioComponentProperty ,
22
22
} from '@aws-amplify/codegen-ui' ;
23
+ import { factory } from 'typescript' ;
23
24
import {
24
25
getFixedComponentPropValueExpression ,
25
26
getComponentPropName ,
@@ -36,6 +37,10 @@ import {
36
37
hasChildrenProp ,
37
38
buildConcatExpression ,
38
39
parseNumberOperand ,
40
+ getStateName ,
41
+ escapePropertyValue ,
42
+ buildBindingExpression ,
43
+ filterScriptingPatterns ,
39
44
} from '../react-component-render-helper' ;
40
45
41
46
import { assertASTMatchesSnapshot } from './__utils__' ;
@@ -357,4 +362,272 @@ describe('react-component-render-helper', () => {
357
362
) ;
358
363
} ) ;
359
364
} ) ;
365
+
366
+ describe ( 'getStateName' , ( ) => {
367
+ it ( 'should correctly format state name by combining component name and property' , ( ) => {
368
+ const stateReference = {
369
+ componentName : 'UserProfile' ,
370
+ property : 'firstName' ,
371
+ } ;
372
+
373
+ const result = getStateName ( stateReference ) ;
374
+
375
+ expect ( result ) . toBe ( 'userProfileFirstName' ) ;
376
+ } ) ;
377
+
378
+ it ( 'should handle single word component names and properties' , ( ) => {
379
+ const stateReference = {
380
+ componentName : 'Button' ,
381
+ property : 'active' ,
382
+ } ;
383
+
384
+ const result = getStateName ( stateReference ) ;
385
+
386
+ expect ( result ) . toBe ( 'buttonActive' ) ;
387
+ } ) ;
388
+
389
+ it ( 'should handle empty strings' , ( ) => {
390
+ const stateReference = {
391
+ componentName : '' ,
392
+ property : '' ,
393
+ } ;
394
+
395
+ const result = getStateName ( stateReference ) ;
396
+
397
+ expect ( result ) . toBe ( '' ) ;
398
+ } ) ;
399
+
400
+ it ( 'should handle special characters if sanitizeName is implemented' , ( ) => {
401
+ const stateReference = {
402
+ componentName : 'User$Profile' ,
403
+ property : 'first@Name' ,
404
+ } ;
405
+
406
+ const result = getStateName ( stateReference ) ;
407
+
408
+ expect ( result ) . toBe ( 'userDollarProfileFirstAtSymbolName' ) ;
409
+ } ) ;
410
+ } ) ;
411
+
412
+ describe ( 'filterScriptingPatterns' , ( ) => {
413
+ it ( 'should keep alphanumeric characters' , ( ) => {
414
+ expect ( filterScriptingPatterns ( 'abc123' ) ) . toBe ( 'abc123' ) ;
415
+ expect ( filterScriptingPatterns ( 'ABC789' ) ) . toBe ( 'ABC789' ) ;
416
+ } ) ;
417
+
418
+ it ( 'should keep allowed special characters' , ( ) => {
419
+ expect ( filterScriptingPatterns ( 'hello.world' ) ) . toBe ( 'hello.world' ) ;
420
+ expect ( filterScriptingPatterns ( 'first,second' ) ) . toBe ( 'first,second' ) ;
421
+ expect ( filterScriptingPatterns ( 'under_score' ) ) . toBe ( 'under_score' ) ;
422
+ expect ( filterScriptingPatterns ( 'dash-here' ) ) . toBe ( 'dash-here' ) ;
423
+ } ) ;
424
+
425
+ it ( 'should remove disallowed special characters' , ( ) => {
426
+ expect ( filterScriptingPatterns ( 'hello@world' ) ) . toBe ( 'hello@world' ) ;
427
+ expect ( filterScriptingPatterns ( 'test#123' ) ) . toBe ( 'test#123' ) ;
428
+ expect ( filterScriptingPatterns ( 'special!chars' ) ) . toBe ( 'special!chars' ) ;
429
+ expect ( filterScriptingPatterns ( 'remove$signs' ) ) . toBe ( 'remove$signs' ) ;
430
+ } ) ;
431
+
432
+ it ( 'should handle spaces correctly' , ( ) => {
433
+ expect ( filterScriptingPatterns ( 'hello world' ) ) . toBe ( 'hello world' ) ;
434
+ expect ( filterScriptingPatterns ( ' extra spaces ' ) ) . toBe ( 'extra spaces' ) ;
435
+ expect ( filterScriptingPatterns ( '\ttab\nspace' ) ) . toBe ( 'tab\nspace' ) ;
436
+ } ) ;
437
+
438
+ it ( 'should handle eval strings' , ( ) => {
439
+ expect ( filterScriptingPatterns ( "eval('if(!window.x){alert(document.domain);window.x=1}')" ) ) . toBe ( '' ) ;
440
+ // eslint-disable-next-line no-script-url
441
+ expect ( filterScriptingPatterns ( 'javascript:alert(1)' ) ) . toBe ( '' ) ;
442
+ } ) ;
443
+
444
+ it ( 'should handle empty strings' , ( ) => {
445
+ expect ( filterScriptingPatterns ( '' ) ) . toBe ( '' ) ;
446
+ expect ( filterScriptingPatterns ( ' ' ) ) . toBe ( '' ) ;
447
+ } ) ;
448
+
449
+ it ( 'should handle non-string inputs' , ( ) => {
450
+ expect ( filterScriptingPatterns ( null as any ) ) . toBe ( '' ) ;
451
+ expect ( filterScriptingPatterns ( undefined as any ) ) . toBe ( '' ) ;
452
+ } ) ;
453
+
454
+ it ( 'should handle mixed content correctly' , ( ) => {
455
+ expect ( filterScriptingPatterns ( 'Hello, World! @ #123' ) ) . toBe ( 'Hello, World! @ #123' ) ;
456
+ expect ( filterScriptingPatterns ( 'user.name@domain.com' ) ) . toBe ( 'user.name@domain.com' ) ;
457
+ expect ( filterScriptingPatterns ( 'path/to/file.txt' ) ) . toBe ( 'path/to/file.txt' ) ;
458
+ } ) ;
459
+
460
+ it ( 'should preserve multiple allowed special characters' , ( ) => {
461
+ expect ( filterScriptingPatterns ( 'item1,item2.item3-item4_item5' ) ) . toBe ( 'item1,item2.item3-item4_item5' ) ;
462
+ } ) ;
463
+
464
+ it ( 'should handle unicode characters' , ( ) => {
465
+ expect ( filterScriptingPatterns ( 'héllo wörld' ) ) . toBe ( 'héllo wörld' ) ;
466
+ expect ( filterScriptingPatterns ( '←↑→↓' ) ) . toBe ( '←↑→↓' ) ;
467
+ expect ( filterScriptingPatterns ( '🌟star' ) ) . toBe ( '🌟star' ) ;
468
+ } ) ;
469
+
470
+ it ( 'should handle complex combinations' , ( ) => {
471
+ const complexInput = `
472
+ Hello! This is a "complex" test-case...
473
+ With @multiple# lines & special chars.
474
+ 123_456-789.000
475
+ ` ;
476
+
477
+ expect ( filterScriptingPatterns ( complexInput ) ) . toBe (
478
+ `Hello! This is a "complex" test-case...
479
+ With @multiple# lines & special chars.
480
+ 123_456-789.000` ,
481
+ ) ;
482
+ } ) ;
483
+ } ) ;
484
+
485
+ describe ( 'escapePropertyName' , ( ) => {
486
+ describe ( 'Reserved Keywords' , ( ) => {
487
+ it ( 'should append "Prop" to JavaScript reserved keywords' , ( ) => {
488
+ expect ( escapePropertyValue ( 'class' ) ) . toBe ( 'classProp' ) ;
489
+ expect ( escapePropertyValue ( 'function' ) ) . toBe ( 'functionProp' ) ;
490
+ expect ( escapePropertyValue ( 'var' ) ) . toBe ( 'varProp' ) ;
491
+ } ) ;
492
+
493
+ it ( 'should not modify non-reserved words' , ( ) => {
494
+ expect ( escapePropertyValue ( 'user' ) ) . toBe ( 'user' ) ;
495
+ expect ( escapePropertyValue ( 'name' ) ) . toBe ( 'name' ) ;
496
+ expect ( escapePropertyValue ( 'address' ) ) . toBe ( 'address' ) ;
497
+ } ) ;
498
+ } ) ;
499
+
500
+ describe ( 'Sanitization' , ( ) => {
501
+ it ( 'should sanitize invalid JavaScript identifiers' , ( ) => {
502
+ expect ( escapePropertyValue ( 'user-name' ) ) . toBe ( 'user-name' ) ;
503
+ expect ( escapePropertyValue ( 'first.last' ) ) . toBe ( 'first.last' ) ;
504
+ expect ( escapePropertyValue ( 'special@char' ) ) . toBe ( 'special@char' ) ;
505
+ } ) ;
506
+
507
+ it ( 'should handle spaces in property names' , ( ) => {
508
+ expect ( escapePropertyValue ( 'first name' ) ) . toBe ( 'first name' ) ;
509
+ expect ( escapePropertyValue ( ' spaced ' ) ) . toBe ( 'spaced' ) ;
510
+ } ) ;
511
+
512
+ it ( 'should preserve valid characters' , ( ) => {
513
+ expect ( escapePropertyValue ( 'validName123' ) ) . toBe ( 'validName123' ) ;
514
+ expect ( escapePropertyValue ( '_privateVar' ) ) . toBe ( '_privateVar' ) ;
515
+ expect ( escapePropertyValue ( '$specialVar' ) ) . toBe ( '$specialVar' ) ;
516
+ } ) ;
517
+ } ) ;
518
+ } ) ;
519
+
520
+ describe ( 'buildBindingExpression' , ( ) => {
521
+ it ( 'should return an empty string with dangerous text' , ( ) => {
522
+ const propertyValue = "eval('if(!window.x){alert(document.domain);window.x=1}')" ;
523
+ const prop = {
524
+ bindingProperties : {
525
+ property : propertyValue ,
526
+ } ,
527
+ } ;
528
+
529
+ const result = buildBindingExpression ( prop ) ;
530
+
531
+ expect ( ( result as any ) . text ) . toBe ( '' ) ;
532
+ } ) ;
533
+
534
+ it ( 'should create property access chain for data attributes' , ( ) => {
535
+ const prop = {
536
+ bindingProperties : {
537
+ property : 'value' ,
538
+ field : 'data-test' ,
539
+ } ,
540
+ } ;
541
+
542
+ const result = buildBindingExpression ( prop ) ;
543
+
544
+ expect ( result . kind ) . toBe ( 204 ) ;
545
+ expect ( ( result as any ) . name . escapedText ) . toBe ( 'data-test' ) ;
546
+ } ) ;
547
+
548
+ it ( 'should return original string containing similar, but not dangerous text' , ( ) => {
549
+ const propertyValue = 'evaluate if window alert document domain window' ;
550
+ const prop = {
551
+ bindingProperties : {
552
+ property : propertyValue ,
553
+ } ,
554
+ } ;
555
+
556
+ const result = buildBindingExpression ( prop ) ;
557
+
558
+ expect ( ( result as any ) . text ) . toBe ( propertyValue ) ;
559
+ } ) ;
560
+
561
+ it ( 'should create a simple identifier when no field is present' , ( ) => {
562
+ const prop = {
563
+ bindingProperties : {
564
+ property : 'userName' ,
565
+ } ,
566
+ } ;
567
+
568
+ const result = buildBindingExpression ( prop ) ;
569
+
570
+ // Check that it's an identifier with the correct text
571
+ expect ( result . kind ) . toBe ( factory . createIdentifier ( '' ) . kind ) ;
572
+ expect ( ( result as any ) . text ) . toBe ( 'userName' ) ;
573
+ } ) ;
574
+
575
+ it ( 'should handle reserved JavaScript keywords in property names' , ( ) => {
576
+ const prop = {
577
+ bindingProperties : {
578
+ property : 'class' , // 'class' is a reserved keyword
579
+ } ,
580
+ } ;
581
+
582
+ const result = buildBindingExpression ( prop ) ;
583
+
584
+ // Should be escaped as 'classProp'
585
+ expect ( ( result as any ) . text ) . toBe ( 'classProp' ) ;
586
+ } ) ;
587
+
588
+ it ( 'should sanitize invalid characters in property names' , ( ) => {
589
+ const prop = {
590
+ bindingProperties : {
591
+ property : 'user@name!' ,
592
+ } ,
593
+ } ;
594
+
595
+ const result = buildBindingExpression ( prop ) ;
596
+
597
+ // Should be sanitized
598
+ expect ( ( result as any ) . text ) . toBe ( 'user@name!' ) ;
599
+ } ) ;
600
+
601
+ it ( 'should generate snapshot for simple property' , ( ) => {
602
+ const prop = {
603
+ bindingProperties : {
604
+ property : 'simpleProperty' ,
605
+ } ,
606
+ } ;
607
+
608
+ assertASTMatchesSnapshot ( buildBindingExpression ( prop ) ) ;
609
+ } ) ;
610
+
611
+ it ( 'should generate snapshot for property with field' , ( ) => {
612
+ const prop = {
613
+ bindingProperties : {
614
+ property : 'user' ,
615
+ field : 'address' ,
616
+ } ,
617
+ } ;
618
+
619
+ assertASTMatchesSnapshot ( buildBindingExpression ( prop ) ) ;
620
+ } ) ;
621
+
622
+ it ( 'should generate snapshot for property with nested field access' , ( ) => {
623
+ const prop = {
624
+ bindingProperties : {
625
+ property : 'data' ,
626
+ field : 'nested.field' ,
627
+ } ,
628
+ } ;
629
+
630
+ assertASTMatchesSnapshot ( buildBindingExpression ( prop ) ) ;
631
+ } ) ;
632
+ } ) ;
360
633
} ) ;
0 commit comments