|
9 | 9 | CHART_AXIS_NOT_SELECTED, |
10 | 10 | } from './validate-chart-bindings.js'; |
11 | 11 | import { explainRule } from './rule-explanations.js'; |
| 12 | +import { runRuntimeAuthoringRules } from './runtime-gate.js'; |
| 13 | +import { REFERENCE_INTEGRITY_RULES } from './reference-integrity-suite.js'; |
12 | 14 |
|
13 | 15 | // [#22161] Each finding of the two converted ids is one verdict sentence; the |
14 | 16 | // reasoning it used to carry is the id's `os explain` entry. Every call below |
@@ -851,6 +853,150 @@ describe('validateChartBindings — list-view charts', () => { |
851 | 853 | }); |
852 | 854 | }); |
853 | 855 |
|
| 856 | +// [#26847] The two standalone `views[]` shapes `PUT /api/v1/meta/view` carries, |
| 857 | +// and the publish door itself. Measured on main before the change: a chart list |
| 858 | +// view naming no declared dataset, or a measure its dataset lacks, published |
| 859 | +// 200 through that door as a ViewItem record, a flattened overlay and a |
| 860 | +// container `listViews` member alike, while `os validate` refused the file. |
| 861 | +describe('[#26847] list-view charts at the view and object publish doors', () => { |
| 862 | + const chart = (over: Record<string, unknown> = {}) => ({ |
| 863 | + chartType: 'bar', |
| 864 | + dataset: 'task_metrics', |
| 865 | + dimensions: ['status'], |
| 866 | + values: ['est_hours'], |
| 867 | + ...over, |
| 868 | + }); |
| 869 | + const listView = (over: Record<string, unknown> = {}) => ({ |
| 870 | + type: 'chart', |
| 871 | + label: 'Hours by status', |
| 872 | + data: { provider: 'object', object: 'showcase_task' }, |
| 873 | + columns: ['status'], |
| 874 | + chart: chart(over), |
| 875 | + }); |
| 876 | + const SHAPES: Array<[string, (over?: Record<string, unknown>) => Record<string, unknown>, string]> = [ |
| 877 | + [ |
| 878 | + 'a ViewItem record', |
| 879 | + (over) => ({ name: 'showcase_task.hours', object: 'showcase_task', viewKind: 'list', label: 'Hours', config: listView(over) }), |
| 880 | + 'views[0].config.chart', |
| 881 | + ], |
| 882 | + [ |
| 883 | + 'a flattened list overlay', |
| 884 | + (over) => ({ name: 'showcase_task.hours', object: 'showcase_task', viewKind: 'list', ...listView(over) }), |
| 885 | + 'views[0].chart', |
| 886 | + ], |
| 887 | + [ |
| 888 | + 'a container listViews member', |
| 889 | + (over) => ({ name: 'showcase_task', object: 'showcase_task', listViews: { hours: listView(over) } }), |
| 890 | + 'views[0].listViews.hours.chart', |
| 891 | + ], |
| 892 | + ]; |
| 893 | + |
| 894 | + describe.each(SHAPES)('the rule walks %s', (_label, view, at) => { |
| 895 | + it('chart-dataset-unknown at `chart.dataset`', () => { |
| 896 | + const findings = validateChartBindings({ ...baseStack(), views: [view({ dataset: 'no_such_dataset' })] }); |
| 897 | + expect(findings.map((f) => [f.rule, f.severity, f.path])).toEqual([[CHART_DATASET_UNKNOWN, 'error', `${at}.dataset`]]); |
| 898 | + }); |
| 899 | + |
| 900 | + it('chart-measure-unknown at `chart.values[i]`', () => { |
| 901 | + const findings = validateChartBindings({ ...baseStack(), views: [view({ values: ['est_hours', 'ghost_measure'] })] }); |
| 902 | + expect(findings.map((f) => [f.rule, f.severity, f.path])).toEqual([[CHART_MEASURE_UNKNOWN, 'error', `${at}.values[1]`]]); |
| 903 | + }); |
| 904 | + |
| 905 | + it('CONTROL — a fully resolved chart reports nothing', () => { |
| 906 | + expect(validateChartBindings({ ...baseStack(), views: [view()] })).toEqual([]); |
| 907 | + }); |
| 908 | + }); |
| 909 | + |
| 910 | + it('a ViewItem record is judged at `config` only — its envelope carries no chart to walk twice', () => { |
| 911 | + const item = { ...SHAPES[0][1]({ dataset: 'no_such_dataset' }), chart: chart({ dataset: 'no_such_dataset' }) }; |
| 912 | + expect(validateChartBindings({ ...baseStack(), views: [item] }).map((f) => f.path)).toEqual([ |
| 913 | + 'views[0].config.chart.dataset', |
| 914 | + ]); |
| 915 | + }); |
| 916 | + |
| 917 | + it('an object list view names a dataset in a stack that declares none: refused, not skipped', () => { |
| 918 | + const findings = validateChartBindings({ |
| 919 | + objects: [{ name: 'showcase_task', fields: {}, listViews: { hours: listView() } }], |
| 920 | + }); |
| 921 | + expect(findings.map((f) => [f.rule, f.path])).toEqual([ |
| 922 | + [CHART_DATASET_UNKNOWN, 'objects[0].listViews.hours.chart.dataset'], |
| 923 | + ]); |
| 924 | + }); |
| 925 | + |
| 926 | + describe('at the runtime publish door', () => { |
| 927 | + const { datasets } = baseStack(); |
| 928 | + const objects = [ |
| 929 | + { |
| 930 | + name: 'showcase_task', |
| 931 | + label: 'Task', |
| 932 | + sharingModel: 'private', |
| 933 | + fields: { |
| 934 | + status: { type: 'text', label: 'Status' }, |
| 935 | + estimate_hours: { type: 'number', label: 'Estimate' }, |
| 936 | + }, |
| 937 | + }, |
| 938 | + ]; |
| 939 | + const chartRules = (fs: Array<{ rule: string; path: string }>) => |
| 940 | + fs.filter((f) => f.rule.startsWith('chart-')).map((f) => [f.rule, f.path]); |
| 941 | + |
| 942 | + it('`validateChartBindings` reaches `view` and `object` writes', () => { |
| 943 | + for (const type of ['view', 'object']) { |
| 944 | + expect( |
| 945 | + REFERENCE_INTEGRITY_RULES.find((r) => r.name === 'validateChartBindings')?.runtimeTypes, |
| 946 | + type, |
| 947 | + ).toContain(type); |
| 948 | + } |
| 949 | + }); |
| 950 | + |
| 951 | + describe.each(SHAPES)('a view written as %s', (_label, view, at) => { |
| 952 | + const write = (over?: Record<string, unknown>) => |
| 953 | + runRuntimeAuthoringRules({ type: 'view', item: view(over), context: { objects, datasets } }); |
| 954 | + |
| 955 | + it('⭐ LIT — a chart naming no declared dataset is REFUSED at `chart.dataset`', () => { |
| 956 | + const result = write({ dataset: 'no_such_dataset' }); |
| 957 | + expect(chartRules(result.errors)).toEqual([[CHART_DATASET_UNKNOWN, `${at}.dataset`]]); |
| 958 | + }); |
| 959 | + |
| 960 | + it('⭐ LIT — a chart whose `values` names no measure of its dataset is REFUSED at `chart.values[i]`', () => { |
| 961 | + const result = write({ values: ['ghost_measure'] }); |
| 962 | + expect(chartRules(result.errors)).toEqual([[CHART_MEASURE_UNKNOWN, `${at}.values[0]`]]); |
| 963 | + }); |
| 964 | + |
| 965 | + it('⭐ CONTROL — the same view bound to a declared dataset and measure publishes clean', () => { |
| 966 | + const result = write(); |
| 967 | + expect(result.rulesRun).toContain('validateReferenceIntegrity'); |
| 968 | + expect(result.errors).toEqual([]); |
| 969 | + }); |
| 970 | + }); |
| 971 | + |
| 972 | + describe('an object write carrying a built-in chart list view', () => { |
| 973 | + const write = (over?: Record<string, unknown>) => |
| 974 | + runRuntimeAuthoringRules({ |
| 975 | + type: 'object', |
| 976 | + item: { ...objects[0], listViews: { hours: { ...listView(over), data: undefined } } }, |
| 977 | + context: { objects, datasets }, |
| 978 | + }); |
| 979 | + |
| 980 | + it('⭐ LIT — an unknown dataset is REFUSED', () => { |
| 981 | + // The door names an object entry by name rather than position. |
| 982 | + expect(chartRules(write({ dataset: 'no_such_dataset' }).errors)).toEqual([ |
| 983 | + [CHART_DATASET_UNKNOWN, 'objects.showcase_task.listViews.hours.chart.dataset'], |
| 984 | + ]); |
| 985 | + }); |
| 986 | + |
| 987 | + it('⭐ LIT — an unknown measure is REFUSED', () => { |
| 988 | + expect(chartRules(write({ values: ['ghost_measure'] }).errors)).toEqual([ |
| 989 | + [CHART_MEASURE_UNKNOWN, 'objects.showcase_task.listViews.hours.chart.values[0]'], |
| 990 | + ]); |
| 991 | + }); |
| 992 | + |
| 993 | + it('⭐ CONTROL — a resolved chart publishes clean', () => { |
| 994 | + expect(write().errors).toEqual([]); |
| 995 | + }); |
| 996 | + }); |
| 997 | + }); |
| 998 | +}); |
| 999 | + |
854 | 1000 | describe('validateChartBindings — dataset-bound page chart components', () => { |
855 | 1001 | it('checks dimensions/values and a ChartConfig-style yAxis', () => { |
856 | 1002 | const findings = validateChartBindings({ |
|
0 commit comments