|
| 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#8266, the RENDER half — what a `dataKey` naming a column the rows do |
| 11 | + * not carry actually draws. |
| 12 | + * |
| 13 | + * `plugin-dashboard/src/__tests__/DashboardChart.countSeriesKey-8266.test.tsx` |
| 14 | + * pins which column the two dashboard relays name. That is necessary and not |
| 15 | + * sufficient: a seam assertion cannot tell a honoured binding from an ignored |
| 16 | + * one, and the whole claim of that card is that the mismatch is SILENT. So this |
| 17 | + * file renders the real chain — `ChartRenderer` → `normalizeChartSchema` → |
| 18 | + * `AdvancedChartImpl` — over the rows a fieldless count actually returns, once |
| 19 | + * under each key, and reads the DOM. |
| 20 | + * |
| 21 | + * It lives here because `recharts` resolves inside `plugin-charts` alone, so |
| 22 | + * this is the only package that can mock `ResponsiveContainer` to a measured |
| 23 | + * box and count marks at all (the same reason |
| 24 | + * `plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.dom.test.tsx` |
| 25 | + * states for its own split). |
| 26 | + * |
| 27 | + * ## Measured on `origin/main` `0fa7a9c83`, before any fix |
| 28 | + * |
| 29 | + * dataKey 'value' (what the relays composed): surface drawn, x ticks |
| 30 | + * ["open","paid"], 0 `.recharts-bar`, 0 `.recharts-rectangle`, NO |
| 31 | + * `[data-chart-error]`, NO `chart-empty-state`. |
| 32 | + * dataKey 'count' (the column the rows carry): 1 `.recharts-bar`, |
| 33 | + * 2 `.recharts-rectangle`, y ticks 0..8. |
| 34 | + * |
| 35 | + * Same rows, same harness — which is what makes the zero a statement about the |
| 36 | + * binding rather than about the harness. The zero arm is kept as a pin |
| 37 | + * deliberately: it is the reason the seam file's assertions are worth making, |
| 38 | + * and if a future guard DOES start refusing this shape loudly, this is the test |
| 39 | + * that must be re-decided rather than a silent behaviour change nobody notices. |
| 40 | + * |
| 41 | + * ⚠️ Mark counts are harness-bound (`ResponsiveContainer` is fixed at 480x320 |
| 42 | + * here); they are re-derived in this file and never carried in from another. |
| 43 | + */ |
| 44 | + |
| 45 | +import React from 'react'; |
| 46 | +import { describe, it, expect, vi, afterEach } from 'vitest'; |
| 47 | +import { render, screen, cleanup, waitFor } from '@testing-library/react'; |
| 48 | + |
| 49 | +// Recharts measures via ResizeObserver, which reports 0x0 under the headless |
| 50 | +// DOM, so nothing paints. Fix its size — the shim every render test in this |
| 51 | +// package uses. |
| 52 | +vi.mock('recharts', async () => { |
| 53 | + const actual = await vi.importActual<any>('recharts'); |
| 54 | + return { |
| 55 | + ...actual, |
| 56 | + ResponsiveContainer: ({ children }: any) => |
| 57 | + React.cloneElement(children, { width: 480, height: 320 }), |
| 58 | + }; |
| 59 | +}); |
| 60 | + |
| 61 | +import { ChartRenderer } from './ChartRenderer'; |
| 62 | +import { aggregateRecords } from './ObjectChart'; |
| 63 | + |
| 64 | +afterEach(cleanup); |
| 65 | + |
| 66 | +/** |
| 67 | + * Not transcribed — produced by the very builder the row projection uses, so a |
| 68 | + * change to the projected column name lands in this fixture instead of leaving |
| 69 | + * the pin asserting against a shape the product stopped emitting. |
| 70 | + */ |
| 71 | +const COUNT_ROWS = aggregateRecords( |
| 72 | + [ |
| 73 | + { status: 'open' }, { status: 'open' }, |
| 74 | + { status: 'paid' }, { status: 'paid' }, { status: 'paid' }, { status: 'paid' }, { status: 'paid' }, |
| 75 | + ], |
| 76 | + { function: 'count', groupBy: 'status' }, |
| 77 | +); |
| 78 | + |
| 79 | +const drawWith = async (dataKey: string) => { |
| 80 | + const { container } = render( |
| 81 | + <ChartRenderer |
| 82 | + schema={{ |
| 83 | + chartType: 'bar', |
| 84 | + data: COUNT_ROWS, |
| 85 | + xAxisKey: 'status', |
| 86 | + series: [{ dataKey }], |
| 87 | + isAnimationActive: false, |
| 88 | + } as any} |
| 89 | + />, |
| 90 | + ); |
| 91 | + // `AdvancedChartImpl` is lazy — wait for the real outcome, not the skeleton. |
| 92 | + // Either terminal state satisfies this, so a refusal is never mistaken for a |
| 93 | + // timeout and a timeout is never read as "it drew nothing". |
| 94 | + await waitFor(() => { |
| 95 | + if (!container.querySelector('.recharts-surface') && !container.querySelector('[data-chart-error]')) { |
| 96 | + throw new Error('neither a plot nor a refusal'); |
| 97 | + } |
| 98 | + }, { timeout: 5000 }); |
| 99 | + return { |
| 100 | + marks: container.querySelectorAll('.recharts-rectangle').length, |
| 101 | + series: container.querySelectorAll('.recharts-bar').length, |
| 102 | + refusal: container.querySelector('[data-chart-error]')?.getAttribute('data-chart-error') ?? null, |
| 103 | + emptyState: !!screen.queryByTestId('chart-empty-state'), |
| 104 | + ticks: Array.from(container.querySelectorAll('.recharts-cartesian-axis-tick-value')).map((n) => n.textContent), |
| 105 | + }; |
| 106 | +}; |
| 107 | + |
| 108 | +describe('a fieldless count projects its value under "count" (objectui#8266)', () => { |
| 109 | + it('is the shape the row builder emits — the premise the rest of the file rests on', () => { |
| 110 | + expect(COUNT_ROWS).toEqual([ |
| 111 | + { status: 'open', count: 2 }, |
| 112 | + { status: 'paid', count: 5 }, |
| 113 | + ]); |
| 114 | + }); |
| 115 | + |
| 116 | + it('draws its marks when the series names that column', async () => { |
| 117 | + const drawn = await drawWith('count'); |
| 118 | + expect(drawn.refusal).toBeNull(); |
| 119 | + expect(drawn.series).toBe(1); |
| 120 | + expect(drawn.marks).toBe(2); |
| 121 | + // The values really reached an axis, so "it drew" is not just a container. |
| 122 | + expect(drawn.ticks).toEqual(expect.arrayContaining(['open', 'paid', '0', '2', '4'])); |
| 123 | + }); |
| 124 | + |
| 125 | + it('draws NOTHING, silently, when the series names "value" instead', async () => { |
| 126 | + const drawn = await drawWith('value'); |
| 127 | + // The failure this card is about: a plot frame with the categories on it… |
| 128 | + expect(drawn.ticks).toEqual(['open', 'paid']); |
| 129 | + // …and not one mark in it. |
| 130 | + expect(drawn.marks).toBe(0); |
| 131 | + expect(drawn.series).toBe(0); |
| 132 | + // …and nothing anywhere says so. Both guards this renderer carries decline: |
| 133 | + // `hasNoCategoryKey` is satisfied (the rows DO have `status`) and |
| 134 | + // `hasNoPlottableSeries` keys on `series: []`, which this is not. |
| 135 | + expect(drawn.refusal).toBeNull(); |
| 136 | + expect(drawn.emptyState).toBe(false); |
| 137 | + }); |
| 138 | +}); |
0 commit comments