@@ -2,13 +2,19 @@ import { describe, it, expect } from 'vitest';
22import {
33 DashboardSchema ,
44 DashboardWidgetSchema ,
5+ DashboardHeaderSchema ,
6+ DashboardHeaderActionSchema ,
7+ WidgetMeasureSchema ,
58 Dashboard ,
69 WidgetColorVariantSchema ,
710 WidgetActionTypeSchema ,
811 GlobalFilterSchema ,
912 GlobalFilterOptionsFromSchema ,
1013 type Dashboard as DashboardType ,
1114 type DashboardWidget ,
15+ type DashboardHeader ,
16+ type DashboardHeaderAction ,
17+ type WidgetMeasure ,
1218 type GlobalFilter ,
1319 type GlobalFilterOptionsFrom ,
1420} from './dashboard.zod' ;
@@ -1162,3 +1168,272 @@ describe('DashboardSchema - enhanced globalFilters', () => {
11621168 expect ( dashboard . globalFilters ! [ 2 ] . targetWidgets ) . toEqual ( [ 'revenue_chart' ] ) ;
11631169 } ) ;
11641170} ) ;
1171+
1172+ // ============================================================================
1173+ // Protocol Enhancement Tests: DashboardHeaderSchema (#714)
1174+ // ============================================================================
1175+
1176+ describe ( 'DashboardHeaderActionSchema' , ( ) => {
1177+ it ( 'should accept valid header action' , ( ) => {
1178+ const result = DashboardHeaderActionSchema . parse ( {
1179+ label : 'Export PDF' ,
1180+ actionUrl : '/export/pdf' ,
1181+ } ) ;
1182+ expect ( result . label ) . toBe ( 'Export PDF' ) ;
1183+ expect ( result . actionUrl ) . toBe ( '/export/pdf' ) ;
1184+ } ) ;
1185+
1186+ it ( 'should accept action with all fields' , ( ) => {
1187+ const result = DashboardHeaderActionSchema . parse ( {
1188+ label : 'Run Report' ,
1189+ actionUrl : 'generate_report_flow' ,
1190+ actionType : 'flow' ,
1191+ icon : 'play' ,
1192+ } ) ;
1193+ expect ( result . actionType ) . toBe ( 'flow' ) ;
1194+ expect ( result . icon ) . toBe ( 'play' ) ;
1195+ } ) ;
1196+
1197+ it ( 'should accept i18n label' , ( ) => {
1198+ const result = DashboardHeaderActionSchema . parse ( {
1199+ label : { key : 'actions.export' , defaultValue : 'Export' } ,
1200+ actionUrl : '/export' ,
1201+ } ) ;
1202+ expect ( result . label ) . toEqual ( { key : 'actions.export' , defaultValue : 'Export' } ) ;
1203+ } ) ;
1204+
1205+ it ( 'should reject action without required fields' , ( ) => {
1206+ expect ( ( ) => DashboardHeaderActionSchema . parse ( { label : 'Test' } ) ) . toThrow ( ) ;
1207+ expect ( ( ) => DashboardHeaderActionSchema . parse ( { actionUrl : '/test' } ) ) . toThrow ( ) ;
1208+ expect ( ( ) => DashboardHeaderActionSchema . parse ( { } ) ) . toThrow ( ) ;
1209+ } ) ;
1210+ } ) ;
1211+
1212+ describe ( 'DashboardHeaderSchema' , ( ) => {
1213+ it ( 'should accept empty header with defaults' , ( ) => {
1214+ const result = DashboardHeaderSchema . parse ( { } ) ;
1215+ expect ( result . showTitle ) . toBe ( true ) ;
1216+ expect ( result . showDescription ) . toBe ( true ) ;
1217+ expect ( result . actions ) . toBeUndefined ( ) ;
1218+ } ) ;
1219+
1220+ it ( 'should accept header with showTitle/showDescription overrides' , ( ) => {
1221+ const result = DashboardHeaderSchema . parse ( {
1222+ showTitle : false ,
1223+ showDescription : false ,
1224+ } ) ;
1225+ expect ( result . showTitle ) . toBe ( false ) ;
1226+ expect ( result . showDescription ) . toBe ( false ) ;
1227+ } ) ;
1228+
1229+ it ( 'should accept header with actions' , ( ) => {
1230+ const result = DashboardHeaderSchema . parse ( {
1231+ actions : [
1232+ { label : 'Export' , actionUrl : '/export/pdf' , icon : 'download' } ,
1233+ { label : 'Share' , actionUrl : 'share_modal' , actionType : 'modal' , icon : 'share' } ,
1234+ ] ,
1235+ } ) ;
1236+ expect ( result . actions ) . toHaveLength ( 2 ) ;
1237+ expect ( result . actions ! [ 0 ] . label ) . toBe ( 'Export' ) ;
1238+ expect ( result . actions ! [ 1 ] . actionType ) . toBe ( 'modal' ) ;
1239+ } ) ;
1240+ } ) ;
1241+
1242+ describe ( 'DashboardSchema - header' , ( ) => {
1243+ it ( 'should accept dashboard with header configuration' , ( ) => {
1244+ const result = DashboardSchema . parse ( {
1245+ name : 'sales_dashboard' ,
1246+ label : 'Sales Dashboard' ,
1247+ description : 'Q4 sales performance' ,
1248+ header : {
1249+ showTitle : true ,
1250+ showDescription : true ,
1251+ actions : [
1252+ { label : 'Export PDF' , actionUrl : '/export/pdf' , icon : 'download' } ,
1253+ ] ,
1254+ } ,
1255+ widgets : [ ] ,
1256+ } ) ;
1257+ expect ( result . header ) . toBeDefined ( ) ;
1258+ expect ( result . header ! . showTitle ) . toBe ( true ) ;
1259+ expect ( result . header ! . actions ) . toHaveLength ( 1 ) ;
1260+ } ) ;
1261+
1262+ it ( 'should accept dashboard without header (backward compat)' , ( ) => {
1263+ const result = DashboardSchema . parse ( {
1264+ name : 'simple_dash' ,
1265+ label : 'Simple' ,
1266+ widgets : [ ] ,
1267+ } ) ;
1268+ expect ( result . header ) . toBeUndefined ( ) ;
1269+ } ) ;
1270+
1271+ it ( 'should accept dashboard with header hiding title/description' , ( ) => {
1272+ const result = DashboardSchema . parse ( {
1273+ name : 'minimal_header_dash' ,
1274+ label : 'Minimal' ,
1275+ header : {
1276+ showTitle : false ,
1277+ showDescription : false ,
1278+ } ,
1279+ widgets : [ ] ,
1280+ } ) ;
1281+ expect ( result . header ! . showTitle ) . toBe ( false ) ;
1282+ expect ( result . header ! . showDescription ) . toBe ( false ) ;
1283+ } ) ;
1284+
1285+ it ( 'should work with Dashboard factory' , ( ) => {
1286+ const dashboard = Dashboard . create ( {
1287+ name : 'executive_dash' ,
1288+ label : 'Executive Dashboard' ,
1289+ description : 'Key business metrics' ,
1290+ header : {
1291+ actions : [
1292+ { label : 'Export' , actionUrl : '/export' , actionType : 'url' , icon : 'download' } ,
1293+ { label : 'Refresh' , actionUrl : 'refresh_flow' , actionType : 'flow' , icon : 'refresh' } ,
1294+ ] ,
1295+ } ,
1296+ widgets : [
1297+ { title : 'Revenue' , type : 'metric' , layout : { x : 0 , y : 0 , w : 3 , h : 2 } } ,
1298+ ] ,
1299+ } ) ;
1300+ expect ( dashboard . header ! . showTitle ) . toBe ( true ) ;
1301+ expect ( dashboard . header ! . actions ) . toHaveLength ( 2 ) ;
1302+ expect ( dashboard . header ! . actions ! [ 1 ] . actionType ) . toBe ( 'flow' ) ;
1303+ } ) ;
1304+ } ) ;
1305+
1306+ // ============================================================================
1307+ // Protocol Enhancement Tests: WidgetMeasureSchema / multi-measure pivot (#714)
1308+ // ============================================================================
1309+
1310+ describe ( 'WidgetMeasureSchema' , ( ) => {
1311+ it ( 'should accept minimal measure' , ( ) => {
1312+ const result = WidgetMeasureSchema . parse ( {
1313+ valueField : 'amount' ,
1314+ } ) ;
1315+ expect ( result . valueField ) . toBe ( 'amount' ) ;
1316+ expect ( result . aggregate ) . toBe ( 'count' ) ;
1317+ } ) ;
1318+
1319+ it ( 'should accept measure with all fields' , ( ) => {
1320+ const result = WidgetMeasureSchema . parse ( {
1321+ valueField : 'amount' ,
1322+ aggregate : 'sum' ,
1323+ label : 'Total Amount' ,
1324+ format : '$0,0.00' ,
1325+ } ) ;
1326+ expect ( result . aggregate ) . toBe ( 'sum' ) ;
1327+ expect ( result . label ) . toBe ( 'Total Amount' ) ;
1328+ expect ( result . format ) . toBe ( '$0,0.00' ) ;
1329+ } ) ;
1330+
1331+ it ( 'should accept measure with i18n label' , ( ) => {
1332+ const result = WidgetMeasureSchema . parse ( {
1333+ valueField : 'quantity' ,
1334+ aggregate : 'avg' ,
1335+ label : { key : 'measures.avg_qty' , defaultValue : 'Average Quantity' } ,
1336+ } ) ;
1337+ expect ( result . label ) . toEqual ( { key : 'measures.avg_qty' , defaultValue : 'Average Quantity' } ) ;
1338+ } ) ;
1339+
1340+ it ( 'should accept all aggregate functions' , ( ) => {
1341+ const aggregates = [ 'count' , 'sum' , 'avg' , 'min' , 'max' ] as const ;
1342+ aggregates . forEach ( aggregate => {
1343+ expect ( ( ) => WidgetMeasureSchema . parse ( { valueField : 'f' , aggregate } ) ) . not . toThrow ( ) ;
1344+ } ) ;
1345+ } ) ;
1346+
1347+ it ( 'should reject measure without valueField' , ( ) => {
1348+ expect ( ( ) => WidgetMeasureSchema . parse ( { } ) ) . toThrow ( ) ;
1349+ expect ( ( ) => WidgetMeasureSchema . parse ( { aggregate : 'sum' } ) ) . toThrow ( ) ;
1350+ } ) ;
1351+ } ) ;
1352+
1353+ describe ( 'DashboardWidgetSchema - measures (multi-measure pivot)' , ( ) => {
1354+ it ( 'should accept pivot widget with measures' , ( ) => {
1355+ const widget = DashboardWidgetSchema . parse ( {
1356+ title : 'Sales by Region and Product' ,
1357+ type : 'pivot' ,
1358+ object : 'opportunity' ,
1359+ categoryField : 'region' ,
1360+ measures : [
1361+ { valueField : 'amount' , aggregate : 'sum' , label : 'Total Amount' , format : '$0,0' } ,
1362+ { valueField : 'amount' , aggregate : 'avg' , label : 'Avg Deal Size' , format : '$0,0.00' } ,
1363+ { valueField : 'amount' , aggregate : 'count' , label : 'Deal Count' } ,
1364+ ] ,
1365+ layout : { x : 0 , y : 0 , w : 12 , h : 6 } ,
1366+ } ) ;
1367+ expect ( widget . measures ) . toHaveLength ( 3 ) ;
1368+ expect ( widget . measures ! [ 0 ] . aggregate ) . toBe ( 'sum' ) ;
1369+ expect ( widget . measures ! [ 1 ] . aggregate ) . toBe ( 'avg' ) ;
1370+ expect ( widget . measures ! [ 2 ] . aggregate ) . toBe ( 'count' ) ;
1371+ } ) ;
1372+
1373+ it ( 'should accept widget without measures (backward compat)' , ( ) => {
1374+ const result = DashboardWidgetSchema . parse ( {
1375+ type : 'bar' ,
1376+ object : 'opportunity' ,
1377+ valueField : 'amount' ,
1378+ aggregate : 'sum' ,
1379+ layout : { x : 0 , y : 0 , w : 6 , h : 4 } ,
1380+ } ) ;
1381+ expect ( result . measures ) . toBeUndefined ( ) ;
1382+ } ) ;
1383+
1384+ it ( 'should accept table widget with measures for multi-aggregate' , ( ) => {
1385+ const widget = DashboardWidgetSchema . parse ( {
1386+ title : 'Regional Summary' ,
1387+ type : 'table' ,
1388+ object : 'order' ,
1389+ categoryField : 'region' ,
1390+ measures : [
1391+ { valueField : 'revenue' , aggregate : 'sum' , label : 'Revenue' } ,
1392+ { valueField : 'quantity' , aggregate : 'sum' , label : 'Units Sold' } ,
1393+ { valueField : 'revenue' , aggregate : 'avg' , label : 'Avg Order Value' } ,
1394+ ] ,
1395+ layout : { x : 0 , y : 0 , w : 12 , h : 6 } ,
1396+ } ) ;
1397+ expect ( widget . measures ) . toHaveLength ( 3 ) ;
1398+ } ) ;
1399+
1400+ it ( 'should work in full dashboard with pivot multi-measure' , ( ) => {
1401+ const dashboard = Dashboard . create ( {
1402+ name : 'analytics_dashboard' ,
1403+ label : 'Analytics Dashboard' ,
1404+ header : {
1405+ actions : [
1406+ { label : 'Export CSV' , actionUrl : '/export/csv' , icon : 'download' } ,
1407+ ] ,
1408+ } ,
1409+ widgets : [
1410+ {
1411+ title : 'Revenue' ,
1412+ type : 'metric' ,
1413+ object : 'order' ,
1414+ valueField : 'amount' ,
1415+ aggregate : 'sum' ,
1416+ layout : { x : 0 , y : 0 , w : 4 , h : 2 } ,
1417+ } ,
1418+ {
1419+ title : 'Sales Pivot Analysis' ,
1420+ type : 'pivot' ,
1421+ object : 'opportunity' ,
1422+ categoryField : 'region' ,
1423+ measures : [
1424+ { valueField : 'amount' , aggregate : 'sum' , label : 'Total Revenue' , format : '$0,0' } ,
1425+ { valueField : 'amount' , aggregate : 'count' , label : 'Deals' } ,
1426+ { valueField : 'amount' , aggregate : 'avg' , label : 'Avg Deal' , format : '$0,0.00' } ,
1427+ { valueField : 'margin' , aggregate : 'avg' , label : 'Avg Margin' , format : '0.0%' } ,
1428+ ] ,
1429+ layout : { x : 0 , y : 2 , w : 12 , h : 6 } ,
1430+ } ,
1431+ ] ,
1432+ } ) ;
1433+
1434+ expect ( dashboard . header ! . actions ) . toHaveLength ( 1 ) ;
1435+ expect ( dashboard . widgets [ 1 ] . measures ) . toHaveLength ( 4 ) ;
1436+ expect ( dashboard . widgets [ 1 ] . measures ! [ 0 ] . format ) . toBe ( '$0,0' ) ;
1437+ expect ( dashboard . widgets [ 1 ] . measures ! [ 3 ] . valueField ) . toBe ( 'margin' ) ;
1438+ } ) ;
1439+ } ) ;
0 commit comments