|
| 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#4612 — the SECOND dashboard surface gets the same graceful fallback. |
| 11 | + * |
| 12 | + * `DashboardRenderer` has carried the framework#3320 legacy sentinel since the |
| 13 | + * retirement: a widget still holding the pre-ADR-0021 inline-analytics shape |
| 14 | + * (top-level `object`, no `dataset`, no inline `options.data`) renders a VISIBLE |
| 15 | + * error placeholder prompting a rebind. `DashboardGridLayout` — separately |
| 16 | + * exported, and registered as the `dashboard-grid` SDUI component — had none, so |
| 17 | + * the very same stored metadata fell through to its static-data branch with |
| 18 | + * `data: []`: a silent blank chart, no diagnostic, no path to fix. That is the |
| 19 | + * exact outcome `DashboardRenderer.legacyRetired.test.tsx`'s header says must not |
| 20 | + * happen, on the surface nobody pinned. |
| 21 | + * |
| 22 | + * The detector and the placeholder now live in ONE module |
| 23 | + * (`../legacyRetiredWidget`) with two consumers, because the defect existed |
| 24 | + * precisely because there were two surfaces and one fix. |
| 25 | + * |
| 26 | + * The negative controls are the binding half. `options.data = { provider: |
| 27 | + * 'object', … }` carries its OWN nested `object`/`aggregate` and is a DIFFERENT, |
| 28 | + * still-live authoring surface (the #4600 measurement's do-not-conflate note); |
| 29 | + * dataset widgets and static-data widgets are live too. None of them may acquire |
| 30 | + * the placeholder — on either surface. |
| 31 | + */ |
| 32 | + |
| 33 | +import { describe, it, expect, afterEach } from 'vitest'; |
| 34 | +import React from 'react'; |
| 35 | +import { render, screen, cleanup } from '@testing-library/react'; |
| 36 | +import type { DashboardComponentSchema } from '@object-ui/types'; |
| 37 | +import { DashboardGridLayout } from '../DashboardGridLayout'; |
| 38 | + |
| 39 | +afterEach(cleanup); |
| 40 | + |
| 41 | +/** |
| 42 | + * `object` / `categoryField` / `aggregate` are no longer part of |
| 43 | + * `DashboardWidgetSchema` — the cast mimics stored legacy metadata that predates |
| 44 | + * the ADR-0021 dataset shape, which is the only way this shape can still arrive. |
| 45 | + */ |
| 46 | +const dash = (widget: Record<string, unknown>): DashboardComponentSchema => |
| 47 | + ({ type: 'dashboard', widgets: [widget] }) as unknown as DashboardComponentSchema; |
| 48 | + |
| 49 | +describe('DashboardGridLayout retired legacy widgets (#4612)', () => { |
| 50 | + it.each([ |
| 51 | + ['chart', { id: 'w1', type: 'bar', object: 'invoices', categoryField: 'month', valueField: 'amount', aggregate: 'sum' }], |
| 52 | + // Byte-for-byte `examples/schema-catalog/src/schemas/plugin-dashboard/ |
| 53 | + // filtered-dashboard.json` → `widgets[0]`: the real stored shape, not a |
| 54 | + // fixture invented to match the detector. |
| 55 | + ['catalog bar', { id: 'invoices_by_status', title: 'Invoices by Status', type: 'bar', object: 'invoices', categoryField: 'status', aggregate: 'count' }], |
| 56 | + // The pivot family, whose legacy spelling names `rowField`/`columnField`. |
| 57 | + ['pivot', { id: 'w1', type: 'pivot', object: 'invoices', rowField: 'region', valueField: 'amount' }], |
| 58 | + // `widgets[2]` of the same catalog entry. |
| 59 | + ['metric', { id: 'w1', type: 'metric', object: 'invoices', aggregate: 'count' }], |
| 60 | + ])('renders the visible placeholder for a legacy %s widget', (_kind, widget) => { |
| 61 | + render(<DashboardGridLayout schema={dash(widget)} />); |
| 62 | + expect(screen.getByText(/retired data format/i)).toBeInTheDocument(); |
| 63 | + }); |
| 64 | + |
| 65 | + it('states the rebind affordance verbatim, not merely "something is wrong"', () => { |
| 66 | + // The message IS the fix path — a placeholder that does not say what to do |
| 67 | + // is only a prettier blank. Pinned verbatim, and identically to the |
| 68 | + // DashboardRenderer surface (one shared constant, one wording). |
| 69 | + render(<DashboardGridLayout schema={dash({ id: 'w1', type: 'bar', object: 'invoices', aggregate: 'count' })} />); |
| 70 | + expect( |
| 71 | + screen.getByText('This widget uses a retired data format. Edit it to bind a dataset.'), |
| 72 | + ).toBeInTheDocument(); |
| 73 | + }); |
| 74 | + |
| 75 | + /** |
| 76 | + * The must-not-change half. Each control asserts BOTH that the placeholder is |
| 77 | + * absent AND that the widget was really rendered — otherwise "no placeholder" |
| 78 | + * would also be satisfied by a grid that threw or drew nothing, and the |
| 79 | + * control would pass for the wrong reason. |
| 80 | + */ |
| 81 | + const renderOne = (widget: Record<string, unknown>) => { |
| 82 | + const { container } = render(<DashboardGridLayout schema={dash(widget)} />); |
| 83 | + expect(container.querySelector('[data-testid="grid-layout"]')).toBeInTheDocument(); |
| 84 | + return container; |
| 85 | + }; |
| 86 | + |
| 87 | + it('does NOT show the placeholder for a dataset-bound widget', () => { |
| 88 | + renderOne({ id: 'w1', type: 'bar', dataset: 'invoices', values: ['count'] }); |
| 89 | + expect(screen.queryByText(/retired data format/i)).not.toBeInTheDocument(); |
| 90 | + }); |
| 91 | + |
| 92 | + it('does NOT show the placeholder for an options.data provider widget', () => { |
| 93 | + // The nested `{ provider: 'object', object, aggregate }` config is a |
| 94 | + // separate, LIVE surface. Its `object` is read off `widgetData`, never off |
| 95 | + // the widget top level — conflating the two would retire a working feature. |
| 96 | + renderOne({ |
| 97 | + id: 'w1', |
| 98 | + type: 'bar', |
| 99 | + options: { data: { provider: 'object', object: 'invoices', aggregate: { field: 'amount', function: 'sum' } } }, |
| 100 | + }); |
| 101 | + expect(screen.queryByText(/retired data format/i)).not.toBeInTheDocument(); |
| 102 | + }); |
| 103 | + |
| 104 | + it('does NOT show the placeholder for a static-data widget', () => { |
| 105 | + renderOne({ id: 'w1', type: 'bar', options: { data: [{ name: 'A', value: 1 }] } }); |
| 106 | + expect(screen.queryByText(/retired data format/i)).not.toBeInTheDocument(); |
| 107 | + }); |
| 108 | + |
| 109 | + it('does NOT show the placeholder for a static-data pivot widget', () => { |
| 110 | + // DashboardRenderer's pivot arm returns the placeholder for the WHOLE family |
| 111 | + // because that surface has no pivot renderer at all. This surface does, and |
| 112 | + // a static-data pivot is a live static-data widget — so the shared detector |
| 113 | + // is the inline-analytics sentinel (which the legacy pivot above matches via |
| 114 | + // its top-level `object`), NOT the family arm. Mirroring the family arm here |
| 115 | + // would have retired a working branch. |
| 116 | + renderOne({ id: 'w1', type: 'pivot', options: { data: [{ region: 'EMEA', amount: 1 }] } }); |
| 117 | + expect(screen.queryByText(/retired data format/i)).not.toBeInTheDocument(); |
| 118 | + }); |
| 119 | +}); |
0 commit comments