|
| 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 | + * objectstack#27861 — the READ face of `position-ref` / `permission-ref`. |
| 11 | + * |
| 12 | + * `sys_user_permission_set.permission_set` and `sys_user_position.position` |
| 13 | + * are by-name text columns declaring those widgets. A list or detail cell of |
| 14 | + * either drew the stored api name (`organization_admin_no_bypass`); it now |
| 15 | + * draws the registry label, with the name on `title`, from the catalog the |
| 16 | + * edit widget lists (`listMetadataItems`), read once for every cell. |
| 17 | + * |
| 18 | + * A name the catalog does not hold — or holds without a label — still draws as |
| 19 | + * the name, and so does every state with no answer yet: no data source, a |
| 20 | + * catalog still loading, a catalog that failed. |
| 21 | + */ |
| 22 | +import { describe, it, expect, vi, afterEach } from 'vitest'; |
| 23 | +import React from 'react'; |
| 24 | +import { render, screen, waitFor, cleanup } from '@testing-library/react'; |
| 25 | +import '@testing-library/jest-dom'; |
| 26 | +import { SchemaRendererProvider } from '@object-ui/react'; |
| 27 | +import { getCellRenderer, resolveCellRendererType } from '../../index'; |
| 28 | +import { PermissionRefCell, PositionRefCell } from '../RegistryRefCell'; |
| 29 | + |
| 30 | +const PERMISSION_SETS = [ |
| 31 | + { name: 'organization_admin_no_bypass', label: 'Organization Admin' }, |
| 32 | + { name: 'admin_full_access', label: 'Full Access' }, |
| 33 | + // An item whose author wrote no label reads as its name alone. |
| 34 | + { name: 'auditor' }, |
| 35 | +]; |
| 36 | +const POSITIONS = [ |
| 37 | + { name: 'sales_rep', label: 'Sales Representative' }, |
| 38 | + { name: 'sales_manager', label: 'Sales Manager' }, |
| 39 | +]; |
| 40 | + |
| 41 | +function lister(answer: (type: string) => Promise<Array<{ name: string; label?: string }>>) { |
| 42 | + return { listMetadataItems: vi.fn(answer), find: vi.fn() } as any; |
| 43 | +} |
| 44 | + |
| 45 | +const FIELD = { name: 'permission_set', type: 'text', label: 'Permission Set', widget: 'permission-ref' } as any; |
| 46 | + |
| 47 | +function mountCells(Cell: typeof PermissionRefCell, values: unknown[], dataSource?: unknown) { |
| 48 | + const cells = values.map((value, i) => ( |
| 49 | + <div key={i} data-testid={`cell-${i}`}> |
| 50 | + <Cell value={value} field={FIELD} /> |
| 51 | + </div> |
| 52 | + )); |
| 53 | + return render(dataSource ? <SchemaRendererProvider dataSource={dataSource as any}>{cells}</SchemaRendererProvider> : <>{cells}</>); |
| 54 | +} |
| 55 | + |
| 56 | +function cellText(i: number): HTMLElement { |
| 57 | + return screen.getByTestId(`cell-${i}`).firstElementChild as HTMLElement; |
| 58 | +} |
| 59 | + |
| 60 | +afterEach(() => cleanup()); |
| 61 | + |
| 62 | +describe('objectstack#27861 — the declared widget picks the registry read face', () => { |
| 63 | + it('promotes a textual field declaring `permission-ref` / `position-ref` to its own key and face', () => { |
| 64 | + expect(resolveCellRendererType({ type: 'text', widget: 'permission-ref' })).toBe('permission-ref'); |
| 65 | + expect(resolveCellRendererType({ type: 'text', widget: 'position-ref' })).toBe('position-ref'); |
| 66 | + expect(getCellRenderer('permission-ref')).toBe(PermissionRefCell); |
| 67 | + expect(getCellRenderer('position-ref')).toBe(PositionRefCell); |
| 68 | + // Control: a non-textual base type keeps its own key. |
| 69 | + expect(resolveCellRendererType({ type: 'select', widget: 'permission-ref' })).toBe('select'); |
| 70 | + }); |
| 71 | +}); |
| 72 | + |
| 73 | +describe('objectstack#27861 — `permission-ref` cells draw the set label', () => { |
| 74 | + it("draws each set's label with its api name on title, from ONE catalog read shared by every cell", async () => { |
| 75 | + const ds = lister(async () => PERMISSION_SETS); |
| 76 | + mountCells(PermissionRefCell, ['organization_admin_no_bypass', 'admin_full_access', 'organization_admin_no_bypass'], ds); |
| 77 | + |
| 78 | + await waitFor(() => expect(cellText(0)).toHaveTextContent('Organization Admin')); |
| 79 | + expect(cellText(0)).toHaveAttribute('title', 'organization_admin_no_bypass'); |
| 80 | + expect(cellText(1)).toHaveTextContent('Full Access'); |
| 81 | + expect(cellText(1)).toHaveAttribute('title', 'admin_full_access'); |
| 82 | + expect(cellText(2)).toHaveTextContent('Organization Admin'); |
| 83 | + |
| 84 | + expect(ds.listMetadataItems).toHaveBeenCalledTimes(1); |
| 85 | + expect(ds.listMetadataItems).toHaveBeenCalledWith('permission'); |
| 86 | + }); |
| 87 | + |
| 88 | + it('a cell mounting after the catalog settled draws the label on its first render, with no second read', async () => { |
| 89 | + const ds = lister(async () => PERMISSION_SETS); |
| 90 | + mountCells(PermissionRefCell, ['admin_full_access'], ds); |
| 91 | + await waitFor(() => expect(cellText(0)).toHaveTextContent('Full Access')); |
| 92 | + cleanup(); |
| 93 | + |
| 94 | + mountCells(PermissionRefCell, ['organization_admin_no_bypass'], ds); |
| 95 | + expect(cellText(0)).toHaveTextContent('Organization Admin'); |
| 96 | + expect(ds.listMetadataItems).toHaveBeenCalledTimes(1); |
| 97 | + }); |
| 98 | +}); |
| 99 | + |
| 100 | +describe('objectstack#27861 — `position-ref` cells draw the position label', () => { |
| 101 | + it("draws each position's label with its api name on title, reading the `position` registry", async () => { |
| 102 | + const ds = lister(async () => POSITIONS); |
| 103 | + mountCells(PositionRefCell, ['sales_manager', 'sales_rep'], ds); |
| 104 | + |
| 105 | + await waitFor(() => expect(cellText(0)).toHaveTextContent('Sales Manager')); |
| 106 | + expect(cellText(0)).toHaveAttribute('title', 'sales_manager'); |
| 107 | + expect(cellText(1)).toHaveTextContent('Sales Representative'); |
| 108 | + expect(cellText(1)).toHaveAttribute('title', 'sales_rep'); |
| 109 | + expect(ds.listMetadataItems).toHaveBeenCalledTimes(1); |
| 110 | + expect(ds.listMetadataItems).toHaveBeenCalledWith('position'); |
| 111 | + }); |
| 112 | +}); |
| 113 | + |
| 114 | +describe('objectstack#27861 — THE FALLBACK: a name the catalog cannot label draws as the name', () => { |
| 115 | + it('a name the catalog does not list renders the name itself, beside a listed one that resolves', async () => { |
| 116 | + const ds = lister(async () => PERMISSION_SETS); |
| 117 | + mountCells(PermissionRefCell, ['retired_set', 'admin_full_access', 'auditor'], ds); |
| 118 | + |
| 119 | + // The listed sibling resolving proves the catalog answered before the |
| 120 | + // unknown name is judged. |
| 121 | + await waitFor(() => expect(cellText(1)).toHaveTextContent('Full Access')); |
| 122 | + expect(cellText(0)).toHaveTextContent(/^retired_set$/); |
| 123 | + expect(cellText(0)).toHaveAttribute('title', 'retired_set'); |
| 124 | + // Listed without a label: the name alone, too. |
| 125 | + expect(cellText(2)).toHaveTextContent(/^auditor$/); |
| 126 | + }); |
| 127 | + |
| 128 | + it('with no data source that can list a registry, the cell draws the name', () => { |
| 129 | + mountCells(PermissionRefCell, ['admin_full_access']); |
| 130 | + expect(cellText(0)).toHaveTextContent(/^admin_full_access$/); |
| 131 | + expect(cellText(0)).toHaveAttribute('title', 'admin_full_access'); |
| 132 | + }); |
| 133 | + |
| 134 | + it('while the catalog loads the cell draws the name', () => { |
| 135 | + const ds = lister(() => new Promise(() => {})); |
| 136 | + mountCells(PermissionRefCell, ['admin_full_access'], ds); |
| 137 | + expect(cellText(0)).toHaveTextContent(/^admin_full_access$/); |
| 138 | + }); |
| 139 | + |
| 140 | + it('a failed catalog draws the name, and is not kept: the next cell asks again', async () => { |
| 141 | + let fail = true; |
| 142 | + const ds = lister(async () => { |
| 143 | + if (fail) throw new Error('403'); |
| 144 | + return PERMISSION_SETS; |
| 145 | + }); |
| 146 | + mountCells(PermissionRefCell, ['admin_full_access'], ds); |
| 147 | + await waitFor(() => expect(ds.listMetadataItems).toHaveBeenCalledTimes(1)); |
| 148 | + await Promise.resolve(); |
| 149 | + expect(cellText(0)).toHaveTextContent(/^admin_full_access$/); |
| 150 | + cleanup(); |
| 151 | + |
| 152 | + fail = false; |
| 153 | + mountCells(PermissionRefCell, ['admin_full_access'], ds); |
| 154 | + await waitFor(() => expect(cellText(0)).toHaveTextContent('Full Access')); |
| 155 | + expect(ds.listMetadataItems).toHaveBeenCalledTimes(2); |
| 156 | + }); |
| 157 | + |
| 158 | + it('an empty value draws the shared No-value glyph and reads nothing', () => { |
| 159 | + const ds = lister(async () => PERMISSION_SETS); |
| 160 | + mountCells(PermissionRefCell, [null, ''], ds); |
| 161 | + expect(cellText(0)).toHaveAttribute('data-slot', 'empty-value'); |
| 162 | + expect(cellText(1)).toHaveAttribute('data-slot', 'empty-value'); |
| 163 | + expect(ds.listMetadataItems).not.toHaveBeenCalled(); |
| 164 | + }); |
| 165 | +}); |
0 commit comments