|
| 1 | +/** |
| 2 | + * ObjectUI |
| 3 | + * Copyright (c) 2024-present ObjectStack Inc. |
| 4 | + * |
| 5 | + * This source code is licensed under the MIT license found in the |
| 6 | + * LICENSE file in the root directory of this source tree. |
| 7 | + */ |
| 8 | + |
| 9 | +/** |
| 10 | + * objectui#10334 (objectui#7759 group F residue) — `DashboardComponentSchema.dateRange` |
| 11 | + * takes the spec's AUTHORING member on both faces, by reference. |
| 12 | + * |
| 13 | + * Before: the Zod mirror restated the element as a stripping `z.object` whose |
| 14 | + * `defaultRange` was a bare `z.string()`, while the TypeScript twin bound |
| 15 | + * `defaultRange` to the spec's `DateRangeDefaultRange`. The mirror therefore |
| 16 | + * admitted preset names `tsc` refused (a typo such as `last_7_dayz` parsed green |
| 17 | + * and then silently resolved to no default window) — the `WiderThanDeclared` |
| 18 | + * row `complex.zod.ts#DashboardComponentSchema::dateRange`. |
| 19 | + * |
| 20 | + * `@objectstack/spec` declares the key (`DashboardSchema.dateRange`, a strict |
| 21 | + * object over `DATE_RANGE_DEFAULT_RANGES`), so by objectui#7759 rule 1 and |
| 22 | + * proposal F1 both faces point at that authoring shape: `dateRange` left |
| 23 | + * `DASHBOARD_SPEC_EXCLUDED` and flows through the same projection as every other |
| 24 | + * spec-owned dashboard key. |
| 25 | + * |
| 26 | + * The type half is enforced by `tsconfig.test.json` (chained from this package's |
| 27 | + * `type-check`); the runtime half asserts the mirror gives the spec's verdict and |
| 28 | + * the spec's issue envelope (`code` + `path`), and authors no default. |
| 29 | + */ |
| 30 | + |
| 31 | +import { describe, it, expect } from 'vitest'; |
| 32 | +import type { z } from 'zod'; |
| 33 | +import { DashboardSchema as SpecDashboardSchema, DATE_RANGE_DEFAULT_RANGES } from '@objectstack/spec/ui'; |
| 34 | +import type { Dashboard } from '@objectstack/spec/ui'; |
| 35 | +import type { DashboardComponentSchema } from '../complex'; |
| 36 | +import { DashboardComponentSchema as DashboardMirror, DASHBOARD_SPEC_EXCLUDED } from '../zod/complex.zod'; |
| 37 | + |
| 38 | +type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false; |
| 39 | + |
| 40 | +const dashboard = (dateRange: unknown) => ({ type: 'dashboard', widgets: [], dateRange }); |
| 41 | + |
| 42 | +describe('dateRange: both faces are the spec authoring member (objectui#10334)', () => { |
| 43 | + it('the TypeScript twin and the mirror input equal the spec document member', () => { |
| 44 | + const twin: Equal<DashboardComponentSchema['dateRange'], Dashboard['dateRange']> = true; |
| 45 | + const mirror: Equal<z.input<typeof DashboardMirror>['dateRange'], Dashboard['dateRange']> = true; |
| 46 | + // Control: the bare-string element the mirror used to carry is NOT the spec member. |
| 47 | + const control: Equal< |
| 48 | + { field?: string; defaultRange?: string; allowCustomRange?: boolean } | undefined, |
| 49 | + Dashboard['dateRange'] |
| 50 | + > = false; |
| 51 | + expect([twin, mirror, control]).toEqual([true, true, false]); |
| 52 | + }); |
| 53 | + |
| 54 | + it('is no longer on the exclusion list both faces read', () => { |
| 55 | + expect(DASHBOARD_SPEC_EXCLUDED as readonly string[]).not.toContain('dateRange'); |
| 56 | + }); |
| 57 | +}); |
| 58 | + |
| 59 | +describe('dateRange: the mirror gives the spec verdict and envelope', () => { |
| 60 | + it('refuses a preset name outside DATE_RANGE_DEFAULT_RANGES at dateRange.defaultRange', () => { |
| 61 | + const r = DashboardMirror.safeParse(dashboard({ defaultRange: 'last_7_dayz' })); |
| 62 | + expect(r.success).toBe(false); |
| 63 | + expect(r.error?.issues.map((i) => ({ code: i.code, path: i.path }))).toEqual([ |
| 64 | + { code: 'invalid_value', path: ['dateRange', 'defaultRange'] }, |
| 65 | + ]); |
| 66 | + }); |
| 67 | + |
| 68 | + it('refuses an unknown key inside dateRange instead of stripping it', () => { |
| 69 | + const r = DashboardMirror.safeParse(dashboard({ field: 'created_at', bogus: 1 })); |
| 70 | + expect(r.success).toBe(false); |
| 71 | + expect(r.error?.issues.map((i) => ({ code: i.code, path: i.path }))).toEqual([ |
| 72 | + { code: 'unrecognized_keys', path: ['dateRange'] }, |
| 73 | + ]); |
| 74 | + }); |
| 75 | + |
| 76 | + it('accepts every preset in DATE_RANGE_DEFAULT_RANGES, including the custom sentinel', () => { |
| 77 | + expect(DATE_RANGE_DEFAULT_RANGES).toContain('custom'); |
| 78 | + for (const defaultRange of DATE_RANGE_DEFAULT_RANGES) { |
| 79 | + const r = DashboardMirror.safeParse(dashboard({ field: 'closed_at', defaultRange, allowCustomRange: false })); |
| 80 | + expect(r.success, defaultRange).toBe(true); |
| 81 | + } |
| 82 | + }); |
| 83 | + |
| 84 | + it('authors no default into the parsed document (stripImportedDefaults)', () => { |
| 85 | + const r = DashboardMirror.safeParse(dashboard({ field: 'created_at' })); |
| 86 | + expect(r.success).toBe(true); |
| 87 | + expect(r.success && r.data.dateRange).toEqual({ field: 'created_at' }); |
| 88 | + }); |
| 89 | + |
| 90 | + it('agrees with the spec schema on every sample', () => { |
| 91 | + const specDateRange = SpecDashboardSchema.shape.dateRange; |
| 92 | + const samples: unknown[] = [ |
| 93 | + {}, |
| 94 | + { field: 'created_at', defaultRange: 'last_30_days', allowCustomRange: true }, |
| 95 | + { defaultRange: 'custom' }, |
| 96 | + { defaultRange: 'last_7_dayz' }, |
| 97 | + { defaultRange: 30 }, |
| 98 | + { preset: 'today' }, |
| 99 | + { bogus: 1 }, |
| 100 | + { allowCustomRange: 'yes' }, |
| 101 | + ]; |
| 102 | + for (const sample of samples) { |
| 103 | + expect( |
| 104 | + DashboardMirror.safeParse(dashboard(sample)).success, |
| 105 | + JSON.stringify(sample), |
| 106 | + ).toBe(specDateRange.safeParse(sample).success); |
| 107 | + } |
| 108 | + }); |
| 109 | +}); |
0 commit comments