@finebi/finebi-cli 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +17 -3
  2. package/package.json +4 -3
  3. package/skills/finebi-cli-agent/SKILL.md +8 -4
  4. package/skills/finebi-cli-agent/references/command-workflows.md +8 -4
  5. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  6. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +8 -1018
  7. package/skills/finebi-cli-agent/references/dashboard-dsl-core.md +194 -0
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-examples.md +378 -0
  9. package/skills/finebi-cli-agent/references/dashboard-dsl-items.md +456 -0
  10. package/skills/finebi-cli-agent/references/mapping-rules.md +14 -14
  11. package/skills/finebi-cli-agent/references/scenario-workflows.md +1 -1
  12. package/skills/finebi-cli-agent/references/web-app.md +17 -0
  13. package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +3 -4
  14. package/skills/finebi-cli-agent/references/widget-dsl-api.md +10 -1374
  15. package/skills/finebi-cli-agent/references/widget-dsl-contexts.md +142 -0
  16. package/skills/finebi-cli-agent/references/widget-dsl-core.md +374 -0
  17. package/skills/finebi-cli-agent/references/widget-dsl-encodings.md +457 -0
  18. package/skills/finebi-cli-agent/references/widget-dsl-styles.md +413 -0
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +148 -0
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +79 -55
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +5 -0
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +30 -0
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +36 -0
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +67 -0
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -0
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +38 -0
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +54 -0
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +93 -0
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -0
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -0
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2191 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +16 -0
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +57 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +56 -0
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -0
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -0
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +21 -0
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +48 -0
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -0
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -0
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -0
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -0
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +39 -0
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -0
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -0
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -0
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -0
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -0
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -0
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +42 -0
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +1 -0
  59. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +44 -0
  60. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -0
  61. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -0
  62. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -0
  63. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -0
  64. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +77 -0
  65. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +18 -0
  66. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -0
  67. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +13 -0
  68. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -0
  69. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -0
  70. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -0
  71. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -0
  72. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/styles.css +8 -0
  73. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -0
  74. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -0
  75. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -0
  76. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -0
  77. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -0
  78. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -0
  79. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/calendar.test.mjs +25 -0
  80. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -0
  81. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +96 -0
  82. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -0
  83. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +136 -0
  84. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -0
  85. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +15 -0
  86. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  87. package/src/app.js +148 -0
  88. package/src/config.js +27 -27
  89. package/src/help.js +79 -55
  90. package/src/index.js +5 -0
  91. package/src/install-skill.js +89 -89
  92. package/src/profiles.js +158 -158
  93. package/src/skill-files.js +94 -94
  94. package/templates/web-app/AGENTS.md +30 -0
  95. package/templates/web-app/README.md +36 -0
  96. package/templates/web-app/docs/acceptance.md +67 -0
  97. package/templates/web-app/docs/auth-and-deploy.md +17 -0
  98. package/templates/web-app/docs/data-contract.md +38 -0
  99. package/templates/web-app/docs/design.md +54 -0
  100. package/templates/web-app/docs/filters.md +93 -0
  101. package/templates/web-app/examples/cockpit.config.json +22 -0
  102. package/templates/web-app/examples/retail.config.json +185 -0
  103. package/templates/web-app/index.html +2 -0
  104. package/templates/web-app/package-lock.json +2191 -0
  105. package/templates/web-app/package.json +16 -0
  106. package/templates/web-app/public/app-config.json +11 -0
  107. package/templates/web-app/scripts/dev-proxy.mjs +57 -0
  108. package/templates/web-app/src/App.tsx +56 -0
  109. package/templates/web-app/src/cockpit.css +3 -0
  110. package/templates/web-app/src/components/Chart.tsx +40 -0
  111. package/templates/web-app/src/components/DataTable.tsx +21 -0
  112. package/templates/web-app/src/components/FilterBar.tsx +48 -0
  113. package/templates/web-app/src/components/Insights.tsx +17 -0
  114. package/templates/web-app/src/components/MonthPicker.tsx +32 -0
  115. package/templates/web-app/src/components/OptionFilter.tsx +7 -0
  116. package/templates/web-app/src/components/RangeFilter.tsx +9 -0
  117. package/templates/web-app/src/components/Widget.tsx +39 -0
  118. package/templates/web-app/src/components/calendar-locales.ts +3 -0
  119. package/templates/web-app/src/components/calendar.ts +14 -0
  120. package/templates/web-app/src/components/charts/Bar.tsx +6 -0
  121. package/templates/web-app/src/components/charts/Line.tsx +6 -0
  122. package/templates/web-app/src/components/charts/Pie.tsx +6 -0
  123. package/templates/web-app/src/components/month-picker.css +1 -0
  124. package/templates/web-app/src/finebi/client.ts +42 -0
  125. package/templates/web-app/src/finebi/config.d.ts +1 -0
  126. package/templates/web-app/src/finebi/config.js +44 -0
  127. package/templates/web-app/src/finebi/date.d.ts +1 -0
  128. package/templates/web-app/src/finebi/date.js +5 -0
  129. package/templates/web-app/src/finebi/filters.d.ts +4 -0
  130. package/templates/web-app/src/finebi/filters.js +106 -0
  131. package/templates/web-app/src/finebi/normalize.ts +77 -0
  132. package/templates/web-app/src/finebi/types.ts +18 -0
  133. package/templates/web-app/src/main.tsx +5 -0
  134. package/templates/web-app/src/retail.css +13 -0
  135. package/templates/web-app/src/services/presentation.ts +36 -0
  136. package/templates/web-app/src/services/table-data.ts +47 -0
  137. package/templates/web-app/src/services/view-data.ts +18 -0
  138. package/templates/web-app/src/services/widget-queries.ts +18 -0
  139. package/templates/web-app/src/styles.css +8 -0
  140. package/templates/web-app/tests/browser/fixtures.mjs +28 -0
  141. package/templates/web-app/tests/browser/index.html +16 -0
  142. package/templates/web-app/tests/browser/main.tsx +149 -0
  143. package/templates/web-app/tests/browser/test-page.css +4 -0
  144. package/templates/web-app/tests/browser/vite.config.mjs +13 -0
  145. package/templates/web-app/tests/bundle.test.mjs +19 -0
  146. package/templates/web-app/tests/calendar.test.mjs +25 -0
  147. package/templates/web-app/tests/cockpit.test.mjs +73 -0
  148. package/templates/web-app/tests/proxy.test.mjs +96 -0
  149. package/templates/web-app/tests/retail.test.mjs +31 -0
  150. package/templates/web-app/tests/runtime.test.mjs +136 -0
  151. package/templates/web-app/tsconfig.json +4 -0
  152. package/templates/web-app/vite.config.ts +15 -0
@@ -0,0 +1,19 @@
1
+ import assert from 'node:assert/strict';
2
+ import {test} from 'node:test';
3
+ import {build} from 'vite';
4
+
5
+ test('production entry defers charts and optional controls and only bundles supported date locales',async()=>{
6
+ const result=await build({logLevel:'silent',build:{write:false}});
7
+ const chunks=result.output.filter(item=>item.type==='chunk');
8
+ const initial=new Set();
9
+ const visit=chunk=>{if(initial.has(chunk))return;initial.add(chunk);for(const name of chunk.imports){const imported=chunks.find(c=>c.fileName===name);if(imported)visit(imported);}};
10
+ chunks.filter(c=>c.isEntry).forEach(visit);
11
+ const initialModules=[...initial].flatMap(c=>Object.keys(c.modules));
12
+ assert.ok(!initialModules.some(id=>/node_modules\/(echarts|zrender)\//.test(id)),'ECharts must not be an entry dependency');
13
+ assert.ok(!initialModules.some(id=>/node_modules\/(antd|@rc-component\/picker)\//.test(id)),'optional filter UI must not be an entry dependency');
14
+ const modules=chunks.flatMap(c=>Object.keys(c.modules));
15
+ assert.ok(!modules.some(id=>id.includes('/tests/browser/')),'browser fixtures and their fetch replacement must not enter production');
16
+ const locales=new Set(modules.flatMap(id=>{const match=id.match(/date-fns\/locale\/([^/]+)/);return match?[match[1]]:[];}));
17
+ assert.deepEqual([...locales].filter(name=>!['en-US','zh-CN','_lib','en-US.js','zh-CN.js'].includes(name)),[],'do not bundle unused calendar languages');
18
+ for(const type of ['Bar','Line','Pie'])assert.ok(chunks.some(c=>c.isDynamicEntry&&Object.keys(c.modules).some(id=>id.endsWith(`/charts/${type}.tsx`))),`${type} has an independent lazy entry`);
19
+ });
@@ -0,0 +1,25 @@
1
+ import assert from 'node:assert/strict';
2
+ import {test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+
5
+ test('picker input and month arithmetic preserve the full calendar contract',async()=>{
6
+ const server=await createServer({configFile:false,cacheDir:'temp/calendar-tests',server:{middlewareMode:true,hmr:false}});
7
+ try {
8
+ const {calendarValue,calendarConfig:config}=await server.ssrLoadModule('/src/components/calendar.ts');
9
+ for(const value of ['0001-01-01','0099-02-28','0100-03-01','2000-02-29','2024-02-29','9999-12-31']) {
10
+ const parsed=config.locale.parse('zh_CN',value,['YYYY-MM-DD']);
11
+ assert.ok(parsed,value);
12
+ assert.equal(config.locale.format('zh_CN',parsed,'YYYY-MM-DD'),value);
13
+ assert.equal(config.locale.format('zh_CN',calendarValue(value),'YYYY-MM-DD'),value);
14
+ }
15
+ for(const value of ['0001-02','0099-12','2024-02','9999-12']) {
16
+ const parsed=config.locale.parse('zh_CN',value,['YYYY-MM']);
17
+ assert.equal(config.locale.format('zh_CN',parsed,'YYYY-MM'),value);
18
+ assert.equal(config.locale.format('zh_CN',calendarValue(value),'YYYY-MM'),value);
19
+ }
20
+ for(const value of ['0000-01-01','1900-02-29','2023-02-29','2024-13-01','2024-02-30','']) assert.equal(calendarValue(value),null,value);
21
+ assert.equal(config.locale.parse('zh_CN','2023-02-29',['YYYY-MM-DD']),null);
22
+ assert.equal(config.locale.format('zh_CN',config.getEndDate(calendarValue('0001-02')),'YYYY-MM-DD'),'0001-02-28');
23
+ assert.equal(config.locale.format('zh_CN',config.addMonth(calendarValue('0099-12'),1),'YYYY-MM'),'0100-01');
24
+ } finally {await server.close();}
25
+ });
@@ -0,0 +1,73 @@
1
+ import assert from 'node:assert/strict';
2
+ import {after,test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+ import {createElement} from 'react';
5
+ import {renderToStaticMarkup} from 'react-dom/server';
6
+ const server=await createServer({configFile:false,cacheDir:'temp/vite-cockpit-tests',server:{middlewareMode:true,hmr:false}});
7
+ after(()=>server.close());
8
+ const client=await server.ssrLoadModule('/src/finebi/client.ts');
9
+ const data=await server.ssrLoadModule('/src/services/table-data.ts');
10
+ const queries=await server.ssrLoadModule('/src/services/widget-queries.ts');
11
+ const {Widget}=await server.ssrLoadModule('/src/components/Widget.tsx');
12
+ const config={schemaVersion:'1',title:'Test',basePath:'/bi',layout:'cockpit',dashboardId:'r',directoryEntryId:'directory',maxRows:1000,pageSize:8,
13
+ widgets:[{entryId:'a',name:'A',type:'table',filterNames:['month']},{entryId:'b',name:'B',type:'table',filterNames:[]}],filters:[{name:'month',label:'月份',type:'month'}]};
14
+ const native={previewResult:{resultType:'DETAIL',header:[],items:[],size:10,row:0,hasNext:false}};
15
+ test('retry is only exposed after a settled failure, never while an initial request is pending',()=>{
16
+ const props={binding:config.widgets[0],pageSize:8,onRetry:()=>{}};
17
+ assert.doesNotMatch(renderToStaticMarkup(createElement(Widget,{...props,state:{loading:true}})),/重试组件/);
18
+ assert.match(renderToStaticMarkup(createElement(Widget,{...props,state:{loading:false,error:new Error('fixture')}})),/重试组件/);
19
+ });
20
+ test('directory source and verified per-widget scope survive individual queries, never falling back',async()=>{
21
+ const sent=[];
22
+ const fetcher=async(url,init)=>{sent.push({url,...JSON.parse(init.body)});return new Response(JSON.stringify({success:true,data:{a:native,b:native}}));};
23
+ await client.queryWidget(config,config.widgets[0],{month:'2024-02'},undefined,fetcher);
24
+ await client.queryWidget(config,config.widgets[1],{month:'2024-02'},undefined,fetcher);
25
+ assert.deepEqual(sent.map(s=>s.reportEntryId),['directory','directory']);
26
+ assert.deepEqual(sent.map(s=>s.widgetIds),[['a'],['b']]);
27
+ assert.deepEqual(sent.map(s=>s.refer),[{month:['2024-02']},{}]);
28
+ let calls=0;
29
+ await assert.rejects(client.queryWidget(config,config.widgets[0],{},undefined,async()=>{calls++;return new Response('{}',{status:403});}),e=>e.code==='PERMISSION_DENIED');
30
+ assert.equal(calls,1);
31
+ });
32
+ test('component loader limits concurrency, publishes independent failures and respects cancellation',async()=>{
33
+ const updates=[];let active=0,peak=0;
34
+ const many={...config,widgets:[...config.widgets,...Array.from({length:6},(_,i)=>({entryId:'extra-'+i,name:'Extra',type:'table'}))]};
35
+ await queries.loadWidgets(many,{},new AbortController().signal,(id,state)=>updates.push([id,state]),async(_url,init)=>{
36
+ active++;peak=Math.max(peak,active);await new Promise(r=>setTimeout(r,5));active--;
37
+ const id=JSON.parse(init.body).widgetIds[0];return id==='a'?new Response('{}',{status:500}):new Response(JSON.stringify({success:true,data:{[id]:native}}));
38
+ });
39
+ assert.equal(peak,3);assert.ok(updates.find(([id,s])=>id==='0:a'&&s.error));assert.ok(updates.find(([id,s])=>id==='1:b'&&s.result));
40
+ const controller=new AbortController();controller.abort();let calls=0;
41
+ await queries.loadWidgets(config,{},controller.signal,()=>assert.fail('no stale update'),async()=>{calls++;});assert.equal(calls,0);
42
+ });
43
+ test('same native widget can have separate display scopes; aborted old responses publish nothing',async()=>{
44
+ const duplicate={...config,widgets:[config.widgets[0],{...config.widgets[0],filterNames:[]}]};
45
+ const complete=[];
46
+ await queries.loadWidgets(duplicate,{month:'2024-02'},new AbortController().signal,(key,state)=>{if(state.result)complete.push(key);},async()=>new Response(JSON.stringify({success:true,data:{a:native}})));
47
+ assert.deepEqual(complete.sort(),['0:a','1:a']);
48
+ const controller=new AbortController(),updates=[];
49
+ await queries.loadWidgets(config,{},controller.signal,(_key,state)=>updates.push(state),async()=>{controller.abort();return new Response(JSON.stringify({success:true,data:{a:native,b:native}}));});
50
+ assert.ok(updates.every(s=>s.loading));
51
+ });
52
+ const cell=(value,state='AVAILABLE',display=value)=>({value,display,state,reason:null});
53
+ const dataset={id:'main',returnedRowCount:4,columns:[{fieldId:'x',key:'c0',name:'名字',type:'STRING'},{fieldId:'v',key:'c1',name:'金额',type:'DECIMAL'}],rows:[
54
+ {kind:'DATA',cells:[cell('=HYPERLINK("x")'),cell('9007199254740993.01')]},
55
+ {kind:'DATA',cells:[cell('B'),cell('9007199254740993.02','AVAILABLE','9,007,199,254,740,993.02元')]},
56
+ {kind:'UNKNOWN',cells:[cell('C'),cell(null,'RESTRICTED','***')]},
57
+ {kind:'DATA',cells:[cell('D'),cell(null,'NULL',null)]},
58
+ ]};
59
+ test('loaded table sorts decimals exactly, filters and exports matching rows with safe CSV and explicit states',()=>{
60
+ assert.equal(data.compareDecimal('9007199254740993.01','9007199254740993.02'),-1);
61
+ assert.equal(data.compareDecimal('-1.001e3','-1001'),0);
62
+ assert.equal(data.compareDecimal('1e-10','0.00000000009'),1);
63
+ assert.equal(data.compareDecimal('1.20','1.2'),0);
64
+ assert.equal(data.compareDecimal('1200','1.2e3'),0);
65
+ assert.equal(data.compareDecimal('-0.001200','-1.2e-3'),0);
66
+ const rows=data.tableRows(dataset,{search:'',sort:{fieldId:'v',direction:'desc'},filters:{}});
67
+ assert.equal(rows[0].cells[0].value,'B');assert.equal(rows[1].cells[0].value,'=HYPERLINK("x")');
68
+ const filtered=data.tableRows(dataset,{search:'',filters:{x:'B'}});assert.equal(filtered.length,1);
69
+ const csv=data.tableCsv(dataset,rows);
70
+ assert.match(csv,/'=HYPERLINK/);assert.match(csv,/受限/);assert.doesNotMatch(csv,/\*\*\*/);assert.match(csv,/UNKNOWN/);
71
+ assert.match(data.tableCsv(dataset,filtered),/9,007,199/);
72
+ assert.throws(()=>data.tableRows(dataset,{search:'',filters:{missing:'x'}}),/字段/);
73
+ });
@@ -0,0 +1,96 @@
1
+ import assert from 'node:assert/strict';
2
+ import {test} from 'node:test';
3
+ import {createServer} from 'node:http';
4
+ import {createDevMiddleware} from '../scripts/dev-proxy.mjs';
5
+
6
+ test('directory proxy pins the published entry and never forwards another source',async()=>{
7
+ const sent=[];
8
+ const middleware=createDevMiddleware({basePath:'/bi',dashboardId:'r',directoryEntryId:'entry',widgets:[{entryId:'w'}],filters:[],maxRows:10},
9
+ {FINEBI_DEV_BASE_URL:'http://bi.example/bi',FINEBI_DEV_TOKEN:'secret'},async(_url,init)=>{sent.push(JSON.parse(init.body));return new Response(JSON.stringify({success:true,data:{}}));});
10
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
11
+ const send=body=>fetch(`http://127.0.0.1:${server.address().port}/bi/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10,...body})});
12
+ try {
13
+ for(const body of [{},{reportEntryId:'other'},{directoryEntryId:'entry'}])assert.equal((await send(body)).status,400);
14
+ assert.equal(sent.length,0);assert.equal((await send({reportEntryId:'entry'})).status,200);assert.equal(sent[0].reportEntryId,'entry');
15
+ }finally{await new Promise(r=>server.close(r));}
16
+ });
17
+
18
+ test('development identity stays on server and cross-site or out-of-scope requests are rejected', async () => {
19
+ const upstream = [];
20
+ const middleware = createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000}, {FINEBI_DEV_BASE_URL:'http://bi.example/webroot/decision',FINEBI_DEV_USERNAME:'tester',FINEBI_DEV_PASSWORD:'dev-secret'}, async (url,init) => {
21
+ upstream.push({url,init});
22
+ return new Response(JSON.stringify(url.endsWith('/login') ? {data:{accessToken:'session-secret'}} : {success:true,data:{w:{previewResult:{resultType:'DETAIL'}}}}));
23
+ });
24
+ const server=createServer((req,res)=>middleware(req,res,()=>{res.writeHead(404);res.end();}));
25
+ await new Promise(r=>server.listen(0,'127.0.0.1',r));
26
+ const base=`http://127.0.0.1:${server.address().port}`;
27
+ const path='/webroot/decision/v5/api/dashboard/report/r/widgets/query';
28
+ const request=(body,headers={})=>fetch(base+path,{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest',...headers},body:JSON.stringify(body)});
29
+ try {
30
+ const valid={widgetIds:['w'],pageLimit:1000};
31
+ assert.equal((await request(valid,{Origin:'https://evil.example'})).status,403);
32
+ assert.equal((await request({...valid,widgetIds:['other']})).status,400);
33
+ assert.equal((await request({...valid,pageLimit:10001})).status,400);
34
+ assert.equal((await request({...valid,refer:{'未配置的控件':['x']}})).status,400);
35
+ assert.equal(upstream.length,0);
36
+ const result=await request(valid,{Origin:base});
37
+ assert.equal(result.status,200);
38
+ assert.doesNotMatch(await result.text(),/secret/);
39
+ assert.equal(upstream.length,2);
40
+ assert.equal(upstream[1].init.headers.Cookie,'fine_auth_token=session-secret');
41
+ assert.equal(upstream[1].init.redirect,'manual');
42
+ assert.equal((await fetch(base+'/webroot/decision/login',{method:'POST'})).status,403);
43
+ assert.equal((await request(valid)).status,200);
44
+ assert.equal(upstream.length,3,'reuse only the in-memory development session');
45
+ } finally {await new Promise(r=>server.close(r));}
46
+ });
47
+ test('HTML login response clears expired development session and explicit retry logs in again', async()=>{
48
+ let logins=0, queries=0;
49
+ const middleware=createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],filters:[],maxRows:1000},{FINEBI_DEV_BASE_URL:'http://bi.example/webroot/decision',FINEBI_DEV_USERNAME:'tester',FINEBI_DEV_PASSWORD:'secret'},async url=>{
50
+ if(url.endsWith('/login')){logins++;return new Response(JSON.stringify({data:{accessToken:'token-'+logins}}));}
51
+ if(++queries===1)return new Response('<html>登录</html>',{headers:{'content-type':'text/html'}});
52
+ return new Response(JSON.stringify({success:true,data:{w:{previewResult:{resultType:'DETAIL'}}}}));
53
+ });
54
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
55
+ const send=()=>fetch(`http://127.0.0.1:${server.address().port}/webroot/decision/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10})});
56
+ try{const expired=await send();assert.equal(expired.status,401);assert.equal((await expired.json()).errorCode,'AUTH_REQUIRED');assert.equal((await send()).status,200);assert.equal(logins,2);}finally{await new Promise(r=>server.close(r));}
57
+ });
58
+ test('missing credentials gives an explicit auth error without upstream access',async()=>{
59
+ const middleware=createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000},{},()=>{throw Error('must not fetch');});
60
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));
61
+ await new Promise(r=>server.listen(0,'127.0.0.1',r));
62
+ try {
63
+ const r=await fetch(`http://127.0.0.1:${server.address().port}/webroot/decision/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10})});
64
+ assert.equal(r.status,401);assert.equal((await r.json()).errorCode,'AUTH_REQUIRED');
65
+ }finally{await new Promise(r=>server.close(r));}
66
+ });
67
+ test('date proxy accepts real dates without an option allowlist and rejects malformed overrides',async()=>{
68
+ const calls=[];
69
+ const middleware=createDevMiddleware({basePath:'/bi',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000,filters:[{name:'日期',label:'日期',type:'date'},{name:'产品',label:'产品',options:[{label:'橡皮',value:'橡皮'}]}]},
70
+ {FINEBI_DEV_BASE_URL:'http://bi.example/bi',FINEBI_DEV_TOKEN:'secret'},async (_url,init)=>{calls.push(JSON.parse(init.body));return new Response(JSON.stringify({success:true,data:{}}));});
71
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
72
+ const send=refer=>fetch(`http://127.0.0.1:${server.address().port}/bi/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:1000,refer})});
73
+ try {
74
+ for(const refer of [{'日期':['2023-02-29']},{'日期':['2024-02-30']},{'日期':['0000-01-01']},{'日期':['2024-2-01']},{'日期':['']},{'日期':['2024-01-01','2024-02-01']},{'日期':[42]},{'未知':['2024-01-01']},{'产品':['未知']}]) assert.equal((await send(refer)).status,400);
75
+ assert.equal(calls.length,0);
76
+ const refer={'日期':['2024-02-29'],'产品':['橡皮']};assert.equal((await send(refer)).status,200);assert.deepEqual(calls[0].refer,refer);
77
+ assert.equal((await send({})).status,200);assert.deepEqual(calls[1].refer,{});
78
+ }finally{await new Promise(r=>server.close(r));}
79
+ });
80
+
81
+ test('proxy applies the same typed contract to ranges, multi-select, null and unsupported raw values',async()=>{
82
+ const calls=[];
83
+ const middleware=createDevMiddleware({basePath:'/bi',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000,filters:[
84
+ {name:'产品',label:'产品',type:'multi-select',allowNull:true,options:[{label:'A',value:'A'},{label:'B',value:'B'}]},
85
+ {name:'日期',label:'日期',type:'date-range'}, {name:'金额',label:'金额',type:'number'},
86
+ ]},{FINEBI_DEV_BASE_URL:'http://bi.example/bi',FINEBI_DEV_TOKEN:'secret'},async (_url,init)=>{calls.push(JSON.parse(init.body));return new Response(JSON.stringify({success:true,data:{}}));});
87
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
88
+ const send=refer=>fetch(`http://127.0.0.1:${server.address().port}/bi/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10,refer})});
89
+ try {
90
+ for(const refer of [{'产品':['A','B']},{'产品':['A,C']},{'产品':['A,A']},{'产品':['A,']},{'日期_s':['2024-03-01'],'日期_e':['2024-02-29']},{'日期':['2024-01-01']},{'金额':['10,20']},null]) assert.equal((await send(refer)).status,400);
91
+ assert.equal(calls.length,0);
92
+ for(const refer of [{'产品':['A,B'],'日期_s':['2024-02-29'],'金额':['-0.25']},{'产品':['null']},{}]) {
93
+ assert.equal((await send(refer)).status,200);assert.deepEqual(calls.at(-1).refer,refer);
94
+ }
95
+ }finally{await new Promise(r=>server.close(r));}
96
+ });
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import {after,test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+ const server=await createServer({configFile:false,cacheDir:'temp/vite-retail-tests',server:{middlewareMode:true,hmr:false}});
5
+ after(()=>server.close());
6
+ const {formatMetric,formatCategory}=await server.ssrLoadModule('/src/services/presentation.ts');
7
+ const {configErrors}=await import('../src/finebi/config.js');
8
+ const cell=(value,state='AVAILABLE')=>({value,display:null,state,reason:null});
9
+ test('KPI display preserves decimal precision and formats ratios only by explicit configuration',()=>{
10
+ assert.equal(formatMetric(cell('9007199254740993.01'),'number'),'9,007,199,254,740,993.01');
11
+ assert.equal(formatMetric(cell('1.2345678e7'),'number'),'12,345,678');
12
+ assert.equal(formatMetric(cell('0.123456789'),'percent'),'12.35%');
13
+ assert.equal(formatMetric(cell('-0.00005'),'percent'),'-0.01%');
14
+ assert.equal(formatMetric(cell('42.0'),'number'),'42');
15
+ assert.equal(formatMetric(cell('100','RESTRICTED'),'number'),'受限');
16
+ assert.equal(formatMetric(cell(null,'NULL'),'percent'),'—');
17
+ assert.equal(formatMetric(cell('0.5')),'0.5');
18
+ });
19
+ test('month formatting is explicit, timezone-aware and preserves invalid or restricted values',()=>{
20
+ const binding={categoryFormat:'epoch-month',categoryTimeZone:'Asia/Shanghai'};
21
+ const timestamp=String(Date.UTC(2024,1,29,16));
22
+ assert.equal(formatCategory(cell(timestamp),binding),'2024-03');
23
+ assert.equal(formatCategory(cell(timestamp),{}),timestamp);
24
+ assert.equal(formatCategory(cell('not-a-date'),binding),'not-a-date');
25
+ assert.equal(formatCategory(cell(timestamp,'RESTRICTED'),binding),'受限');
26
+ });
27
+ test('display configuration rejects implicit date timezone and invalid formats',()=>{
28
+ const config={schemaVersion:'1',title:'Test',basePath:'/bi',layout:'retail',dashboardId:'r',maxRows:1000,pageSize:8,filters:[],widgets:[{entryId:'w',name:'Trend',type:'line',categoryFieldId:'x',valueFieldId:'y',categoryFormat:'epoch-month',categoryTimeZone:'Asia/Shanghai'}]};
29
+ assert.deepEqual(configErrors(config),[]);
30
+ for(const patch of [{categoryTimeZone:undefined},{categoryTimeZone:'Bad/Timezone'},{categoryFormat:'guess'},{valueFormat:'guess'}])assert.ok(configErrors({...config,widgets:[{...config.widgets[0],...patch}]}).length);
31
+ });
@@ -0,0 +1,136 @@
1
+ import assert from 'node:assert/strict';
2
+ import {after, test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+ const server = await createServer({configFile: false, cacheDir: 'temp/vite-tests', server: {middlewareMode: true, hmr: false}});
5
+ after(() => server.close());
6
+ const {normalize} = await server.ssrLoadModule('/src/finebi/normalize.ts');
7
+ const {queryWidgets, FineBIError} = await server.ssrLoadModule('/src/finebi/client.ts');
8
+ const {chartValues, selectDataset} = await server.ssrLoadModule('/src/services/view-data.ts');
9
+ const detail = () => ({previewResult: {resultType: 'DETAIL', header: [
10
+ {dId: 'd', text: '重复名'}, {dId: 'n', text: '重复名'}, {dId: 't', text: '日期'}
11
+ ], items: [[{value: 'A', text: 'A', authority: true}, {value: '1234.50', formatText: '1,234.50元', authority: true}, {value: '1349020800000', formatText: '2012-10-01', authority: true}]], row: 1000, size: 1, hasNext: true}, widgetBean: {dimensions: {d: {type: 1}, n: {type: 2}, t: {type: 3}}}});
12
+ test('detail preserves field identity, display, decimal and date values with honest range', () => {
13
+ const r = normalize(detail());
14
+ assert.equal(r.schemaVersion, '1');
15
+ assert.equal(r.normalization, 'COMPLETE');
16
+ assert.deepEqual(r.datasets[0].columns.map(c => c.key), ['c0','c1','c2']);
17
+ assert.deepEqual(r.datasets[0].rows[0].cells.map(c => c.value), ['A','1234.50','1349020800000']);
18
+ assert.equal(r.datasets[0].rows[0].cells[1].display, '1,234.50元');
19
+ assert.equal(r.range.total.value, 1000);
20
+ assert.ok(r.warnings.includes('NEXT_PAGE_UNSUPPORTED'));
21
+ });
22
+ test('restricted, display-only, missing, unknown and invalid dates cannot become computable values', () => {
23
+ const source = detail();
24
+ source.previewResult.items = [[{value: 'secret', text: '***', authority: false}, {value: null, text: '20%', authority: true}, {value: '9223372036854775808', authority: true}], []];
25
+ const r = normalize(source);
26
+ assert.equal(r.normalization, 'PARTIAL');
27
+ const cells = r.datasets[0].rows[0].cells;
28
+ assert.equal(cells[0].state, 'RESTRICTED');
29
+ assert.equal(cells[0].value, null);
30
+ assert.equal(cells[1].state, 'UNAVAILABLE');
31
+ assert.equal(cells[2].reason, 'INVALID_TIMESTAMP_VALUE');
32
+ delete source.widgetBean;
33
+ assert.equal(normalize(source).datasets[0].rows[0].cells[1].value, null);
34
+ });
35
+ test('full-looking page does not discard native hasNext; empty is different from unsupported', () => {
36
+ const source = detail(); source.previewResult.row = 1;
37
+ assert.ok(normalize(source).warnings.includes('NEXT_PAGE_UNSUPPORTED'));
38
+ source.previewResult.items = []; source.previewResult.row = 0; source.previewResult.hasNext = false;
39
+ assert.equal(normalize(source).datasets[0].returnedRowCount, 0);
40
+ assert.equal(normalize({previewResult: {resultType: 'BICROSS'}}).normalization, 'UNSUPPORTED');
41
+ });
42
+ test('chart permission placeholders and all supported detail date metadata preserve their semantics', () => {
43
+ const r=normalize({previewResult:{resultType:'CHART',geoms:[{dataModel:{fields:[{id:'n',name:'指标',fieldType:'MEASURE'}],colData:[['***']],rowCount:1}}]}});
44
+ assert.equal(r.datasets[0].rows[0].cells[0].state,'RESTRICTED');
45
+ for(const type of [3,13,15,20]) {const s=detail();s.widgetBean.dimensions.t.type=type;assert.equal(normalize(s).datasets[0].columns[2].type,'DATE');}
46
+ });
47
+ test('chart preserves multiple models, rejects formatted numeric strings and mismatched columns', () => {
48
+ const model = {fields: [{id:'x',name:'地区',fieldType:'DIMENSION'}, {id:'y',name:'金额',fieldType:'MEASURE'}], colData: [['East','West'], [12,'1,234']],rowCount:2};
49
+ const r = normalize({previewResult:{resultType:'CHART',geoms:[{dataModel:model},{dataModel:{...model,colData:[['A'],[]],rowCount:1}}]}});
50
+ assert.equal(r.datasets.length,2);
51
+ assert.equal(r.datasets[0].rows[0].cells[1].value,'12');
52
+ assert.equal(r.datasets[0].rows[1].cells[1].state,'UNAVAILABLE');
53
+ assert.equal(r.normalization,'PARTIAL');
54
+ });
55
+ const config = {schemaVersion:'1',title:'Test',layout:'overview',pageSize:8,basePath:'/webroot/decision',dashboardId:'report',maxRows:1000,widgets:[{entryId:'table',type:'table',name:'明细'}],filters:[{name:'地区',label:'地区',options:[{label:'西部',value:'West'}]}]};
56
+ test('query sends scoped refer, cookie credentials and bounded pageLimit without browser token storage', async () => {
57
+ let sent;
58
+ const r = await queryWidgets(config, {'地区':'West'}, undefined, async (url, init) => {
59
+ sent = {url, ...init};
60
+ return new Response(JSON.stringify({success:true,data:{table:detail()}}));
61
+ });
62
+ assert.equal(sent.credentials,'include');
63
+ assert.equal(sent.headers['X-Requested-With'],'XMLHttpRequest');
64
+ assert.deepEqual(JSON.parse(sent.body).refer, {'地区':['West']});
65
+ assert.equal(JSON.parse(sent.body).pageLimit,1000);
66
+ assert.equal(sent.headers.Authorization,undefined);
67
+ assert.equal(r.table.datasets[0].returnedRowCount,1);
68
+ await assert.rejects(queryWidgets({...config,maxRows:10001},{},undefined,()=>{throw new Error('must not fetch');}), /maxRows/);
69
+ await assert.rejects(queryWidgets(config,{'未知':'x'}), /筛选/);
70
+ });
71
+ test('auth expiry, permission denial, login HTML and missing widget stay distinct from empty data', async () => {
72
+ for (const [response, code] of [[new Response('',{status:302,headers:{Location:'/login'}}),'AUTH_REQUIRED'],[new Response('',{status:401}),'AUTH_REQUIRED'],[new Response('',{status:403}),'PERMISSION_DENIED'],[new Response(JSON.stringify({errorCode:'TIME_OUT'})),'AUTH_REQUIRED'],[new Response('<html>login</html>',{headers:{'content-type':'text/html'}}),'AUTH_REQUIRED'],[new Response(JSON.stringify({success:true,data:{}})),'INVALID_RESPONSE']]) {
73
+ await assert.rejects(queryWidgets(config,{},undefined,async()=>response), e=>e instanceof FineBIError && e.code===code);
74
+ }
75
+ });
76
+ test('view binding requires exact fields and an explicit model for multi-model results', () => {
77
+ const source = {previewResult:{resultType:'CHART',geoms:[{dataModel:{fields:[{id:'x',name:'地区',fieldType:'DIMENSION'},{id:'y',name:'金额',fieldType:'MEASURE'}],colData:[['East','West'],[12,5]],rowCount:2}}]}};
78
+ const r=normalize(source);
79
+ assert.deepEqual(chartValues(selectDataset(r),{categoryFieldId:'x',valueFieldId:'y'}),[{name:'East',value:12},{name:'West',value:5}]);
80
+ assert.throws(()=>chartValues(selectDataset(r),{categoryFieldId:'x',valueFieldId:'missing'}),/字段/);
81
+ r.datasets.push({...r.datasets[0],id:'model1'});
82
+ assert.throws(()=>selectDataset(r),/模型/);
83
+ assert.equal(selectDataset(r,'model1').id,'model1');
84
+ });
85
+ test('date filters accept calendar dates without options and omit cleared dates', async () => {
86
+ const dateConfig = {...config, filters:[{name:'日期',label:'日期',type:'date'},...config.filters]};
87
+ let sent;
88
+ const fetchImpl=async (_url,init)=>{sent=JSON.parse(init.body);return new Response(JSON.stringify({success:true,data:{table:detail()}}));};
89
+ for (const value of ['2012-10-02','2024-02-29','0001-01-01','9999-12-31']) {
90
+ await queryWidgets(dateConfig,{'日期':value},undefined,fetchImpl);
91
+ assert.deepEqual(sent.refer,{'日期':[value]});
92
+ }
93
+ await queryWidgets(dateConfig,{'日期':''},undefined,fetchImpl);
94
+ assert.deepEqual(sent.refer,{});
95
+ for (const value of ['2023-02-29','2024-02-30','2024-04-31','0000-01-01','10000-01-01','2024-13-01','2024-1-01','2024-01-01T00:00:00Z']) {
96
+ await assert.rejects(queryWidgets(dateConfig,{'日期':value},undefined,()=>{assert.fail('invalid date must not be sent');}),/筛选/);
97
+ }
98
+ for (const filter of [{name:'日期',label:'日期',type:'date',options:[]},{name:'日期',label:'日期',type:'unknown'}]) {
99
+ await assert.rejects(queryWidgets({...config,filters:[filter]},{},undefined,()=>{assert.fail('invalid configuration must not be sent');}),/筛选/);
100
+ }
101
+ });
102
+
103
+ test('typed filters encode native multi-value, null and open date ranges without guessing', async () => {
104
+ const filters=[
105
+ {name:'文本',label:'文本',type:'text',allowNull:true},
106
+ {name:'多选',label:'多选',type:'multi-select',options:[{label:'东',value:'East'},{label:'西',value:'West'}]},
107
+ {name:'数值',label:'数值',type:'number'},
108
+ {name:'区间',label:'区间',type:'date-range'},
109
+ {name:'月份',label:'月份',type:'month-range'},
110
+ {name:'年',label:'年',type:'year'}, {name:'季',label:'季',type:'quarter'},
111
+ {name:'标签',label:'标签',type:'time-label',options:[{label:'本月',value:'本月,动态'}]},
112
+ ];
113
+ let sent;
114
+ const send=values=>queryWidgets({...config,filters},values,undefined,async (_url,init)=>{
115
+ sent=JSON.parse(init.body);return new Response(JSON.stringify({success:true,data:{table:detail()}}));
116
+ });
117
+ await send({'文本':' North ','多选':['East','West'],'数值':'12345678901234567890.25','区间':{start:'2024-02-29',end:'2024-03-01'},'月份':{start:'2024-01',end:''},'年':'2024','季':'2024-3','标签':'本月,动态'});
118
+ assert.deepEqual(sent.refer,{'文本':[' North '],'多选':['East,West'],'数值':['12345678901234567890.25'],'区间_s':['2024-02-29'],'区间_e':['2024-03-01'],'月份_s':['2024-01'],'年':['2024'],'季':['2024-3'],'标签':['本月,动态']});
119
+ await send({'文本':null,'多选':[],'区间':{start:'',end:''}});
120
+ assert.deepEqual(sent.refer,{'文本':['null']});
121
+ await send({}); assert.deepEqual(sent.refer,{});
122
+ for(const values of [{'文本':'A,B'},{'文本':'null'},{'多选':['East,West']},{'多选':['East','East']},{'多选':[null]},{'数值':'1e3'},{'区间':{start:'2024-03-02',end:'2024-03-01'}},{'区间':{start:'2024-02-30',end:''}},{'月份':{start:'2024-13',end:''}},{'年':'0000'},{'季':'2024-5'},{'标签':'未知'},{'区间':null},{'未知':''}]) {
123
+ await assert.rejects(send(values),/筛选/);
124
+ }
125
+ });
126
+
127
+ test('filter configuration rejects ambiguous native keys and unrepresentable choices before fetching',async()=>{
128
+ for(const filters of [
129
+ [{name:'范围',label:'范围',type:'date-range'},{name:'范围_s',label:'起始',type:'date'}],
130
+ [{name:'重复',label:'A',type:'text'},{name:'重复',label:'B',type:'date'}],
131
+ [{name:'x',label:'x',options:[{label:'逗号',value:'A,B'}]}],
132
+ [{name:'x',label:'x',options:[{label:'字面空值',value:'null'}]}],
133
+ [{name:'x',label:'x',type:'number-range'}],
134
+ [{name:'x',label:'x',type:'date',allowNull:true}],
135
+ ]) await assert.rejects(queryWidgets({...config,filters},{},undefined,()=>assert.fail('invalid config must not fetch')),/筛选/);
136
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
3
+ "include":["src","vite.config.ts"]
4
+ }
@@ -0,0 +1,15 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {defineConfig, loadEnv} from 'vite';
4
+ // @ts-expect-error The development-only Node module is tested directly with node:test.
5
+ import {createDevMiddleware} from './scripts/dev-proxy.mjs';
6
+ export default defineConfig(({mode}) => ({
7
+ base: './',
8
+ resolve:{alias:[{find:/^date-fns\/locale$/,replacement:fileURLToPath(new URL('./src/components/calendar-locales.ts',import.meta.url))}]},
9
+ server: {host:'127.0.0.1',port:4173,strictPort:true},
10
+ plugins:[{name:'finebi-development-session',apply:'serve',configureServer(server) {
11
+ const config=JSON.parse(readFileSync(new URL('./public/app-config.json',import.meta.url),'utf8'));
12
+ const env={...loadEnv(mode,process.cwd(),'FINEBI_DEV_'),...process.env};
13
+ server.middlewares.use(createDevMiddleware(config,env));
14
+ }}],
15
+ }));