|
3 | 3 | DashboardSchema, |
4 | 4 | DashboardWidgetSchema, |
5 | 5 | Dashboard, |
| 6 | + WidgetColorVariantSchema, |
| 7 | + WidgetActionTypeSchema, |
6 | 8 | type Dashboard as DashboardType, |
7 | 9 | type DashboardWidget, |
8 | 10 | } from './dashboard.zod'; |
@@ -610,3 +612,243 @@ describe('DashboardSchema - dateRange', () => { |
610 | 612 | expect(result.dateRange).toBeUndefined(); |
611 | 613 | }); |
612 | 614 | }); |
| 615 | + |
| 616 | +// ============================================================================ |
| 617 | +// Protocol Enhancement Tests: colorVariant, description, actionUrl/actionType |
| 618 | +// ============================================================================ |
| 619 | + |
| 620 | +describe('WidgetColorVariantSchema', () => { |
| 621 | + it('should accept all color variants', () => { |
| 622 | + const variants = ['default', 'blue', 'teal', 'orange', 'purple', 'success', 'warning', 'danger']; |
| 623 | + variants.forEach(variant => { |
| 624 | + expect(() => WidgetColorVariantSchema.parse(variant)).not.toThrow(); |
| 625 | + }); |
| 626 | + }); |
| 627 | + |
| 628 | + it('should reject invalid color variants', () => { |
| 629 | + expect(() => WidgetColorVariantSchema.parse('red')).toThrow(); |
| 630 | + expect(() => WidgetColorVariantSchema.parse('unknown')).toThrow(); |
| 631 | + }); |
| 632 | +}); |
| 633 | + |
| 634 | +describe('WidgetActionTypeSchema', () => { |
| 635 | + it('should accept all action types', () => { |
| 636 | + const types = ['url', 'modal', 'flow']; |
| 637 | + types.forEach(type => { |
| 638 | + expect(() => WidgetActionTypeSchema.parse(type)).not.toThrow(); |
| 639 | + }); |
| 640 | + }); |
| 641 | + |
| 642 | + it('should reject invalid action types', () => { |
| 643 | + expect(() => WidgetActionTypeSchema.parse('script')).toThrow(); |
| 644 | + expect(() => WidgetActionTypeSchema.parse('invalid')).toThrow(); |
| 645 | + }); |
| 646 | +}); |
| 647 | + |
| 648 | +describe('DashboardWidgetSchema - colorVariant', () => { |
| 649 | + it('should accept widget with colorVariant', () => { |
| 650 | + const widget: DashboardWidget = { |
| 651 | + title: 'Total Revenue', |
| 652 | + type: 'metric', |
| 653 | + colorVariant: 'teal', |
| 654 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 655 | + }; |
| 656 | + const result = DashboardWidgetSchema.parse(widget); |
| 657 | + expect(result.colorVariant).toBe('teal'); |
| 658 | + }); |
| 659 | + |
| 660 | + it('should accept widget without colorVariant (optional)', () => { |
| 661 | + const result = DashboardWidgetSchema.parse({ |
| 662 | + type: 'metric', |
| 663 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 664 | + }); |
| 665 | + expect(result.colorVariant).toBeUndefined(); |
| 666 | + }); |
| 667 | + |
| 668 | + it('should reject invalid colorVariant', () => { |
| 669 | + expect(() => DashboardWidgetSchema.parse({ |
| 670 | + type: 'metric', |
| 671 | + colorVariant: 'neon', |
| 672 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 673 | + })).toThrow(); |
| 674 | + }); |
| 675 | +}); |
| 676 | + |
| 677 | +describe('DashboardWidgetSchema - description', () => { |
| 678 | + it('should accept widget with string description', () => { |
| 679 | + const result = DashboardWidgetSchema.parse({ |
| 680 | + title: 'Revenue', |
| 681 | + description: 'Year-to-date total revenue', |
| 682 | + type: 'metric', |
| 683 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 684 | + }); |
| 685 | + expect(result.description).toBe('Year-to-date total revenue'); |
| 686 | + }); |
| 687 | + |
| 688 | + it('should accept widget with i18n description', () => { |
| 689 | + const result = DashboardWidgetSchema.parse({ |
| 690 | + title: 'Revenue', |
| 691 | + description: { key: 'widgets.revenue.desc', defaultValue: 'Total revenue' }, |
| 692 | + type: 'metric', |
| 693 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 694 | + }); |
| 695 | + expect(result.description).toEqual({ key: 'widgets.revenue.desc', defaultValue: 'Total revenue' }); |
| 696 | + }); |
| 697 | + |
| 698 | + it('should accept widget without description (optional)', () => { |
| 699 | + const result = DashboardWidgetSchema.parse({ |
| 700 | + type: 'metric', |
| 701 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 702 | + }); |
| 703 | + expect(result.description).toBeUndefined(); |
| 704 | + }); |
| 705 | +}); |
| 706 | + |
| 707 | +describe('DashboardWidgetSchema - actionUrl/actionType/actionIcon', () => { |
| 708 | + it('should accept widget with actionUrl and actionType', () => { |
| 709 | + const result = DashboardWidgetSchema.parse({ |
| 710 | + title: 'Open Tickets', |
| 711 | + type: 'metric', |
| 712 | + actionUrl: 'https://example.com/tickets', |
| 713 | + actionType: 'url', |
| 714 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 715 | + }); |
| 716 | + expect(result.actionUrl).toBe('https://example.com/tickets'); |
| 717 | + expect(result.actionType).toBe('url'); |
| 718 | + }); |
| 719 | + |
| 720 | + it('should accept widget with actionIcon', () => { |
| 721 | + const result = DashboardWidgetSchema.parse({ |
| 722 | + title: 'Details', |
| 723 | + type: 'metric', |
| 724 | + actionUrl: '/details', |
| 725 | + actionType: 'url', |
| 726 | + actionIcon: 'external-link', |
| 727 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 728 | + }); |
| 729 | + expect(result.actionIcon).toBe('external-link'); |
| 730 | + }); |
| 731 | + |
| 732 | + it('should accept widget with modal action type', () => { |
| 733 | + const result = DashboardWidgetSchema.parse({ |
| 734 | + title: 'Breakdown', |
| 735 | + type: 'metric', |
| 736 | + actionUrl: 'revenue_breakdown', |
| 737 | + actionType: 'modal', |
| 738 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 739 | + }); |
| 740 | + expect(result.actionType).toBe('modal'); |
| 741 | + }); |
| 742 | + |
| 743 | + it('should accept widget with flow action type', () => { |
| 744 | + const result = DashboardWidgetSchema.parse({ |
| 745 | + title: 'Refresh Data', |
| 746 | + type: 'metric', |
| 747 | + actionUrl: 'refresh_pipeline_flow', |
| 748 | + actionType: 'flow', |
| 749 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 750 | + }); |
| 751 | + expect(result.actionType).toBe('flow'); |
| 752 | + }); |
| 753 | + |
| 754 | + it('should accept widget without action fields (optional)', () => { |
| 755 | + const result = DashboardWidgetSchema.parse({ |
| 756 | + type: 'metric', |
| 757 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 758 | + }); |
| 759 | + expect(result.actionUrl).toBeUndefined(); |
| 760 | + expect(result.actionType).toBeUndefined(); |
| 761 | + expect(result.actionIcon).toBeUndefined(); |
| 762 | + }); |
| 763 | + |
| 764 | + it('should reject invalid actionType', () => { |
| 765 | + expect(() => DashboardWidgetSchema.parse({ |
| 766 | + type: 'metric', |
| 767 | + actionType: 'invalid', |
| 768 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 769 | + })).toThrow(); |
| 770 | + }); |
| 771 | +}); |
| 772 | + |
| 773 | +describe('DashboardWidgetSchema - combined new fields', () => { |
| 774 | + it('should accept KPI widget with all new fields', () => { |
| 775 | + const widget: DashboardWidget = { |
| 776 | + title: 'Revenue', |
| 777 | + description: 'Q4 total revenue across all regions', |
| 778 | + type: 'metric', |
| 779 | + colorVariant: 'success', |
| 780 | + actionUrl: 'https://reports.example.com/revenue', |
| 781 | + actionType: 'url', |
| 782 | + actionIcon: 'external-link', |
| 783 | + object: 'opportunity', |
| 784 | + valueField: 'amount', |
| 785 | + aggregate: 'sum', |
| 786 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 787 | + }; |
| 788 | + |
| 789 | + const result = DashboardWidgetSchema.parse(widget); |
| 790 | + expect(result.description).toBe('Q4 total revenue across all regions'); |
| 791 | + expect(result.colorVariant).toBe('success'); |
| 792 | + expect(result.actionUrl).toBe('https://reports.example.com/revenue'); |
| 793 | + expect(result.actionType).toBe('url'); |
| 794 | + expect(result.actionIcon).toBe('external-link'); |
| 795 | + }); |
| 796 | + |
| 797 | + it('should work in a full dashboard with color-coded KPI cards', () => { |
| 798 | + const dashboard = Dashboard.create({ |
| 799 | + name: 'kpi_dashboard', |
| 800 | + label: 'KPI Dashboard', |
| 801 | + widgets: [ |
| 802 | + { |
| 803 | + title: 'Revenue', |
| 804 | + description: 'Total quarterly revenue', |
| 805 | + type: 'metric', |
| 806 | + colorVariant: 'success', |
| 807 | + object: 'opportunity', |
| 808 | + valueField: 'amount', |
| 809 | + aggregate: 'sum', |
| 810 | + layout: { x: 0, y: 0, w: 3, h: 2 }, |
| 811 | + }, |
| 812 | + { |
| 813 | + title: 'Open Issues', |
| 814 | + description: 'Unresolved support tickets', |
| 815 | + type: 'metric', |
| 816 | + colorVariant: 'warning', |
| 817 | + actionUrl: '/issues', |
| 818 | + actionType: 'url', |
| 819 | + object: 'case', |
| 820 | + aggregate: 'count', |
| 821 | + layout: { x: 3, y: 0, w: 3, h: 2 }, |
| 822 | + }, |
| 823 | + { |
| 824 | + title: 'Critical Bugs', |
| 825 | + description: 'P0/P1 bugs requiring attention', |
| 826 | + type: 'metric', |
| 827 | + colorVariant: 'danger', |
| 828 | + actionUrl: 'bug_triage_flow', |
| 829 | + actionType: 'flow', |
| 830 | + actionIcon: 'alert-triangle', |
| 831 | + object: 'bug', |
| 832 | + aggregate: 'count', |
| 833 | + layout: { x: 6, y: 0, w: 3, h: 2 }, |
| 834 | + }, |
| 835 | + { |
| 836 | + title: 'Team Velocity', |
| 837 | + type: 'bar', |
| 838 | + colorVariant: 'blue', |
| 839 | + object: 'sprint', |
| 840 | + categoryField: 'sprint_name', |
| 841 | + valueField: 'story_points', |
| 842 | + aggregate: 'sum', |
| 843 | + layout: { x: 0, y: 2, w: 12, h: 4 }, |
| 844 | + }, |
| 845 | + ], |
| 846 | + }); |
| 847 | + |
| 848 | + expect(dashboard.widgets).toHaveLength(4); |
| 849 | + expect(dashboard.widgets[0].colorVariant).toBe('success'); |
| 850 | + expect(dashboard.widgets[1].actionUrl).toBe('/issues'); |
| 851 | + expect(dashboard.widgets[2].description).toBe('P0/P1 bugs requiring attention'); |
| 852 | + expect(dashboard.widgets[3].colorVariant).toBe('blue'); |
| 853 | + }); |
| 854 | +}); |
0 commit comments