|
| 1 | +/** |
| 2 | + * ObjectUI |
| 3 | + * Copyright (c) 2024-present ObjectStack Inc. |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +/** |
| 9 | + * objectstack#29284 — the published mirror refuses a dashboard whose |
| 10 | + * `globalFilters` declares one key twice, with the spec's own issue. |
| 11 | + * |
| 12 | + * `@objectstack/spec`'s `DashboardSchema` attaches its duplicate-key refusal |
| 13 | + * (objectstack#29023) to the `globalFilters` ARRAY. `DashboardComponentSchema` |
| 14 | + * rebuilt that array as `z.array(GlobalFilterSchema)`, which kept every element |
| 15 | + * check and dropped the array's own, so two filters under one key passed here |
| 16 | + * while every spec door refused them. The mirror now takes the spec's slot by |
| 17 | + * reference; this file pins that it carries the refusal, and that the |
| 18 | + * refusal is the spec's issue (code, path and message), not a local dialect. |
| 19 | + * |
| 20 | + * A filter's key is its `name`, else its `field`. |
| 21 | + */ |
| 22 | +import { describe, expect, it } from 'vitest'; |
| 23 | +import { DashboardSchema as SpecDashboardSchema } from '@objectstack/spec/ui'; |
| 24 | +import { DashboardComponentSchema, GlobalFilterSchema } from '../zod/complex.zod'; |
| 25 | + |
| 26 | +/** The mirror's node: component discriminator plus the one required slot. */ |
| 27 | +const node = (globalFilters: unknown[]) => ({ type: 'dashboard', widgets: [], globalFilters }); |
| 28 | +/** The spec's dashboard for the same filters. */ |
| 29 | +const specBoard = (globalFilters: unknown[]) => ({ name: 'qa_board', label: 'QA board', widgets: [], globalFilters }); |
| 30 | + |
| 31 | +const customIssues = (result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[]; message: string }> } }) => |
| 32 | + (result.success ? [] : result.error!.issues.filter((i) => i.code === 'custom')) |
| 33 | + .map(({ code, path, message }) => ({ code, path, message })); |
| 34 | + |
| 35 | +const BY_NAME = [ |
| 36 | + { name: 'region', field: 'region', type: 'text' }, |
| 37 | + { name: 'region', field: 'territory', type: 'text' }, |
| 38 | +]; |
| 39 | +const BY_FIELD = [ |
| 40 | + { field: 'status', type: 'text' }, |
| 41 | + { field: 'status', type: 'text' }, |
| 42 | +]; |
| 43 | + |
| 44 | +describe('objectstack#29284 — DashboardComponentSchema refuses a duplicate globalFilters key with the spec\'s issue', () => { |
| 45 | + it('two filters under one `name` are refused at the second one\'s `name`', () => { |
| 46 | + const mirror = customIssues(DashboardComponentSchema.safeParse(node(BY_NAME))); |
| 47 | + expect(mirror).toHaveLength(1); |
| 48 | + expect(mirror[0]).toMatchObject({ code: 'custom', path: ['globalFilters', 1, 'name'] }); |
| 49 | + expect(mirror[0]!.message.startsWith("This dashboard declares the global filter 'region' twice in globalFilters")).toBe(true); |
| 50 | + }); |
| 51 | + |
| 52 | + it('two unnamed filters over one `field` are refused at the second one\'s `field` (the key falls back to the field)', () => { |
| 53 | + const mirror = customIssues(DashboardComponentSchema.safeParse(node(BY_FIELD))); |
| 54 | + expect(mirror).toHaveLength(1); |
| 55 | + expect(mirror[0]).toMatchObject({ code: 'custom', path: ['globalFilters', 1, 'field'] }); |
| 56 | + expect(mirror[0]!.message).toContain('A filter with no `name` is keyed by its `field`.'); |
| 57 | + }); |
| 58 | + |
| 59 | + it.each([ |
| 60 | + ['by name', BY_NAME], |
| 61 | + ['by field fallback', BY_FIELD], |
| 62 | + ])('the refusal %s is the spec door\'s own issue, byte for byte', (_label, filters) => { |
| 63 | + const spec = customIssues(SpecDashboardSchema.safeParse(specBoard(filters))); |
| 64 | + expect(spec).toHaveLength(1); |
| 65 | + expect(customIssues(DashboardComponentSchema.safeParse(node(filters)))).toEqual(spec); |
| 66 | + }); |
| 67 | + |
| 68 | + it('distinct keys parse — a named filter over a field another filter is keyed by is a different key', () => { |
| 69 | + const filters = [ |
| 70 | + { name: 'region', field: 'region', type: 'text' }, |
| 71 | + { name: 'territory', field: 'region', type: 'text' }, |
| 72 | + { field: 'status', type: 'text' }, |
| 73 | + ]; |
| 74 | + expect(SpecDashboardSchema.safeParse(specBoard(filters)).success).toBe(true); |
| 75 | + expect(DashboardComponentSchema.safeParse(node(filters)).success).toBe(true); |
| 76 | + }); |
| 77 | + |
| 78 | + it('the slot\'s element is still the published GlobalFilterSchema, so element checks are unchanged', () => { |
| 79 | + const slot = DashboardComponentSchema.shape.globalFilters; |
| 80 | + const array = slot.unwrap(); |
| 81 | + expect(array.element).toBe(GlobalFilterSchema); |
| 82 | + }); |
| 83 | +}); |
0 commit comments