-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathmetadata-viewer.test.tsx
More file actions
136 lines (123 loc) · 5.33 KB
/
Copy pathmetadata-viewer.test.tsx
File metadata and controls
136 lines (123 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* element:metadata_viewer (ADR-0051) — read-only live metadata views.
*
* Renders the component through ComponentRegistry (the same path a
* ```metadata doc fence and a page node both use) with a mocked metadata
* context, and asserts each of the three view kinds plus graceful
* degradation.
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { MetadataCtx } from '@object-ui/react';
import '../renderers/basic/metadata-viewer';
function metaCtx(getItem: (type: string, name: string) => Promise<any>) {
return {
apps: [], objects: [], dashboards: [], reports: [], pages: [],
loading: false, error: null,
refresh: async () => {}, invalidate: () => {}, ensureType: async () => [],
getItem, getItemsByType: () => [], getTypeStatus: () => 'ready' as const,
};
}
function Viewer({ properties, getItem }: { properties: any; getItem: (t: string, n: string) => Promise<any> }) {
const C = ComponentRegistry.get('element:metadata_viewer');
if (!C) throw new Error('element:metadata_viewer not registered');
return (
<MetadataCtx.Provider value={metaCtx(getItem) as any}>
<C schema={{ type: 'element:metadata_viewer', properties }} />
</MetadataCtx.Provider>
);
}
const TASK_OBJECT = {
name: 'showcase_task',
fields: {
status: {
type: 'select',
options: [
{ value: 'backlog', label: 'Backlog', color: '#94A3B8', default: true },
{ value: 'todo', label: 'To Do', color: '#3B82F6' },
{ value: 'in_progress', label: 'In Progress', color: '#F59E0B' },
{ value: 'done', label: 'Done', color: '#10B981' },
],
},
},
validations: [
{
type: 'state_machine',
name: 'task_status_flow',
label: 'Task Status Flow',
field: 'status',
transitions: {
backlog: ['todo'],
todo: ['in_progress', 'backlog'],
in_progress: ['done'],
done: [],
},
},
],
};
describe('element:metadata_viewer', () => {
it('registers as a component', () => {
expect(ComponentRegistry.get('element:metadata_viewer')).toBeTruthy();
});
describe('state_machine', () => {
it('renders the transition graph with option labels, initial + final badges', async () => {
const getItem = vi.fn(async (type: string) => (type === 'object' ? TASK_OBJECT : null));
render(<Viewer getItem={getItem} properties={{ type: 'state_machine', object: 'showcase_task', name: 'task_status_flow' }} />);
await waitFor(() => expect(screen.getByText('Task Status Flow')).toBeTruthy());
// option label, not the raw value
expect(screen.getAllByText('Backlog').length).toBeGreaterThan(0);
expect(screen.getAllByText('In Progress').length).toBeGreaterThan(0);
// backlog is the default option → initial; done has no outgoing → final
expect(screen.getByText(/initial/i)).toBeTruthy();
expect(screen.getByText(/final/i)).toBeTruthy();
expect(getItem).toHaveBeenCalledWith('object', 'showcase_task');
});
it('warns when the named rule is absent', async () => {
const getItem = vi.fn(async () => ({ name: 'x', validations: [] }));
render(<Viewer getItem={getItem} properties={{ type: 'state_machine', object: 'x', name: 'nope' }} />);
await waitFor(() => expect(screen.getByText(/No state machine/i)).toBeTruthy());
});
});
describe('flow', () => {
const FLOW = {
name: 'reassign',
label: 'Reassign Task',
nodes: [
{ id: 'start', type: 'start', label: 'Start' },
{ id: 's1', type: 'script', label: 'Do Plumbing' },
{ id: 'end', type: 'end', label: 'End' },
],
};
it('folds technical nodes at business altitude (default)', async () => {
const getItem = vi.fn(async () => FLOW);
render(<Viewer getItem={getItem} properties={{ type: 'flow', name: 'reassign' }} />);
await waitFor(() => expect(screen.getByText('Reassign Task')).toBeTruthy());
expect(screen.getByText('Start')).toBeTruthy();
expect(screen.getByText('End')).toBeTruthy();
expect(screen.queryByText('Do Plumbing')).toBeNull(); // script folded
expect(screen.getByText(/technical step.*hidden/i)).toBeTruthy();
});
it('shows technical nodes when detail=technical', async () => {
const getItem = vi.fn(async () => FLOW);
render(<Viewer getItem={getItem} properties={{ type: 'flow', name: 'reassign', detail: 'technical' }} />);
await waitFor(() => expect(screen.getByText('Do Plumbing')).toBeTruthy());
});
});
describe('permission', () => {
it('renders an object CRUD matrix', async () => {
const getItem = vi.fn(async () => ({
name: 'contributor',
label: 'Contributor',
objects: { showcase_task: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false } },
}));
render(<Viewer getItem={getItem} properties={{ type: 'permission', name: 'contributor' }} />);
await waitFor(() => expect(screen.getByText('Contributor')).toBeTruthy());
expect(screen.getByText('showcase_task')).toBeTruthy();
});
});
it('degrades on an unknown view type', () => {
render(<Viewer getItem={async () => null} properties={{ type: 'nonsense' }} />);
expect(screen.getByText(/Unknown metadata view type/i)).toBeTruthy();
});
});