@proulr/sdui-components 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/src/bdc/bdc-compile-pipeline.integration.test.ts +115 -0
- package/src/bdc/compile-bdc-to-wire.test.ts +105 -1
- package/src/bdc/components/bdc-components.test.ts +142 -0
- package/src/bdc/jsx-runtime.test.ts +53 -0
- package/src/catalog/catalog.test.ts +91 -0
- package/src/renderer/lib/discover-profile-formatters.test.ts +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proulr/sdui-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/renderer/index.ts",
|
|
6
6
|
"types": "./src/renderer/index.ts",
|
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"test": "vitest run",
|
|
27
27
|
"test:watch": "vitest",
|
|
28
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
29
|
+
"test:integration:coverage": "vitest run --config vitest.integration.config.ts --coverage",
|
|
28
30
|
"prepublishOnly": "npm run typecheck",
|
|
29
31
|
"lint": "eslint .",
|
|
30
32
|
"lint:fix": "eslint . --fix",
|
|
31
|
-
"version:sync": "node ../scripts/proulr-bump-and-sync-deps.mjs"
|
|
33
|
+
"version:sync": "node ../scripts/proulr-bump-and-sync-deps.mjs",
|
|
34
|
+
"test:coverage": "vitest run --coverage"
|
|
32
35
|
},
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public"
|
|
@@ -67,6 +70,7 @@
|
|
|
67
70
|
"devDependencies": {
|
|
68
71
|
"@types/node": "^22.20.0",
|
|
69
72
|
"@types/react": "~19.2.2",
|
|
73
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
70
74
|
"eslint": "^9.0.0",
|
|
71
75
|
"eslint-config-expo": "~56.0.4",
|
|
72
76
|
"expo-blur": "~56.0.3",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { decodeSduiScreenWire } from '@proulr/sdui-contract';
|
|
3
|
+
import { compileBdcBlock, compileBdcToWire } from './compile-bdc-to-wire';
|
|
4
|
+
import type { BdcElement } from './jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function screen(props: Record<string, unknown>, children: BdcElement[]): BdcElement {
|
|
7
|
+
return { type: 'screen', props, children };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('BDC compile → contract decode integration', () => {
|
|
11
|
+
it('round-trips a full BDC tree including header slots and derived labels', () => {
|
|
12
|
+
const slot: BdcElement = {
|
|
13
|
+
type: 'chip_row',
|
|
14
|
+
props: { id: 'slot' },
|
|
15
|
+
children: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const wire = compileBdcToWire(
|
|
19
|
+
screen(
|
|
20
|
+
{
|
|
21
|
+
id: 'demo.home',
|
|
22
|
+
title: 'Demo',
|
|
23
|
+
data: { city: 'sp' },
|
|
24
|
+
header: {
|
|
25
|
+
title: 'Home',
|
|
26
|
+
headerSearch: true,
|
|
27
|
+
brandMode: true,
|
|
28
|
+
slotBlocks: [{ type: 'ignored' }],
|
|
29
|
+
},
|
|
30
|
+
headerSlot: [slot, { type: 'chip', props: {}, children: [] }],
|
|
31
|
+
actions: {
|
|
32
|
+
go: { type: 'navigate', path: '/x' },
|
|
33
|
+
},
|
|
34
|
+
meta: { bff: 'demo-bff', cache_ttl_ms: 12_000, stale_policy: 'cache_only' },
|
|
35
|
+
},
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
type: 'column',
|
|
39
|
+
props: { gap: 8, id: 'root' },
|
|
40
|
+
children: [
|
|
41
|
+
{
|
|
42
|
+
type: 'button',
|
|
43
|
+
props: { actionId: 'go', style: { paddingTop: 8, marginBottom: 4 } },
|
|
44
|
+
children: [
|
|
45
|
+
{ type: '_text_node', props: { value: ' Press ' }, children: [] },
|
|
46
|
+
{ type: 'text', props: { text: 'me' }, children: [] },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
props: {},
|
|
52
|
+
children: [{ type: '_text_node', props: { value: 'Body' }, children: [] }],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: '_fragment',
|
|
56
|
+
props: {},
|
|
57
|
+
children: [],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'button',
|
|
61
|
+
props: { actionId: 'send_message', label: 'Send' },
|
|
62
|
+
children: [],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(wire.schema_version).toBe(1);
|
|
71
|
+
expect(wire.screen_id).toBe('demo.home');
|
|
72
|
+
expect(wire.header).toMatchObject({
|
|
73
|
+
title: 'Home',
|
|
74
|
+
header_search: true,
|
|
75
|
+
brand_mode: true,
|
|
76
|
+
});
|
|
77
|
+
expect(wire.header?.slot_blocks).toHaveLength(2);
|
|
78
|
+
expect(wire.blocks[0]?.props).toMatchObject({ gap: 8 });
|
|
79
|
+
expect(wire.blocks[0]?.children?.[0]?.props).toMatchObject({
|
|
80
|
+
action_id: 'go',
|
|
81
|
+
label: 'Press me',
|
|
82
|
+
style: { padding_top: 8, margin_bottom: 4 },
|
|
83
|
+
});
|
|
84
|
+
expect(wire.blocks[0]?.children?.[1]?.props?.text).toBe('Body');
|
|
85
|
+
expect(wire.blocks[0]?.children?.[2]?.props).toMatchObject({
|
|
86
|
+
action_id: 'send_message',
|
|
87
|
+
label: 'Send',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const decoded = decodeSduiScreenWire(wire);
|
|
91
|
+
expect(decoded.screenId).toBe('demo.home');
|
|
92
|
+
expect(decoded.blocks).toHaveLength(1);
|
|
93
|
+
expect(decoded.blocks[0]?.type).toBe('column');
|
|
94
|
+
expect(decoded.data).toEqual({ city: 'sp' });
|
|
95
|
+
expect(decoded.meta.bff).toBe('demo-bff');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('rejects non-screen roots and compiles lone text nodes', () => {
|
|
99
|
+
expect(() =>
|
|
100
|
+
compileBdcToWire({ type: 'stack', props: {}, children: [] }),
|
|
101
|
+
).toThrow(/bdc_root_must_be_screen/);
|
|
102
|
+
|
|
103
|
+
const block = compileBdcBlock({
|
|
104
|
+
type: '_text_node',
|
|
105
|
+
props: { value: 'x' },
|
|
106
|
+
children: [],
|
|
107
|
+
});
|
|
108
|
+
expect(block).toEqual({ id: '0', type: 'text', props: { text: 'x' } });
|
|
109
|
+
|
|
110
|
+
const empty = compileBdcToWire(screen({ id: 'empty' }, []));
|
|
111
|
+
expect(empty.data).toEqual({});
|
|
112
|
+
expect(empty.actions).toEqual({});
|
|
113
|
+
expect(empty.meta).toMatchObject({ cache_ttl_ms: 30_000 });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { compileBdcToWire } from './compile-bdc-to-wire';
|
|
2
|
+
import { compileBdcBlock, compileBdcToWire } from './compile-bdc-to-wire';
|
|
3
3
|
import type { BdcElement } from './jsx-runtime';
|
|
4
|
+
import { compileBdcToWire as viaIndex } from './index';
|
|
4
5
|
|
|
5
6
|
function screen(props: Record<string, unknown>, children: BdcElement[]): BdcElement {
|
|
6
7
|
return { type: 'screen', props, children };
|
|
@@ -45,4 +46,107 @@ describe('compileBdcToWire', () => {
|
|
|
45
46
|
compileBdcToWire({ type: 'stack', props: {}, children: [] }),
|
|
46
47
|
).toThrow(/bdc_root_must_be_screen/);
|
|
47
48
|
});
|
|
49
|
+
|
|
50
|
+
it('derives button/text labels from children and maps style keys', () => {
|
|
51
|
+
const wire = compileBdcToWire(
|
|
52
|
+
screen({ id: 'demo.home' }, [
|
|
53
|
+
{
|
|
54
|
+
type: 'button',
|
|
55
|
+
props: { actionId: 'go', style: { paddingTop: 8, marginBottom: 4 } },
|
|
56
|
+
children: [
|
|
57
|
+
{ type: '_text_node', props: { value: ' Press ' }, children: [] },
|
|
58
|
+
{ type: 'text', props: { text: 'me' }, children: [] },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
props: {},
|
|
64
|
+
children: [{ type: '_text_node', props: { value: 'Body' }, children: [] }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: '_fragment',
|
|
68
|
+
props: {},
|
|
69
|
+
children: [],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'column',
|
|
73
|
+
props: { id: 'col-1', children: 'ignored' },
|
|
74
|
+
children: [
|
|
75
|
+
{ type: '_text_node', props: { value: 'skip' }, children: [] },
|
|
76
|
+
{ type: 'spacer', props: {}, children: [] },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
expect(wire.blocks).toHaveLength(3);
|
|
82
|
+
expect(wire.blocks[0]?.props).toMatchObject({
|
|
83
|
+
action_id: 'go',
|
|
84
|
+
label: 'Press me',
|
|
85
|
+
style: { padding_top: 8, margin_bottom: 4 },
|
|
86
|
+
});
|
|
87
|
+
expect(wire.blocks[1]?.props?.text).toBe('Body');
|
|
88
|
+
expect(wire.blocks[2]?.id).toBe('col-1');
|
|
89
|
+
expect(wire.blocks[2]?.children).toHaveLength(1);
|
|
90
|
+
expect(wire.blocks[2]?.children?.[0]?.type).toBe('spacer');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('compiles header slot blocks and defaults meta/actions/data', () => {
|
|
94
|
+
const slot: BdcElement = {
|
|
95
|
+
type: 'chip_row',
|
|
96
|
+
props: { id: 'slot' },
|
|
97
|
+
children: [],
|
|
98
|
+
};
|
|
99
|
+
const wire = compileBdcToWire(
|
|
100
|
+
screen(
|
|
101
|
+
{
|
|
102
|
+
id: 'demo.home',
|
|
103
|
+
header: {
|
|
104
|
+
title: 'Home',
|
|
105
|
+
headerSearch: true,
|
|
106
|
+
slotBlocks: [{ type: 'ignored' }],
|
|
107
|
+
headerSlot: 'ignored',
|
|
108
|
+
},
|
|
109
|
+
headerSlot: slot,
|
|
110
|
+
},
|
|
111
|
+
[],
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
expect(wire.header).toMatchObject({
|
|
115
|
+
title: 'Home',
|
|
116
|
+
header_search: true,
|
|
117
|
+
});
|
|
118
|
+
expect(wire.header?.slot_blocks?.[0]).toMatchObject({ id: 'slot', type: 'chip_row' });
|
|
119
|
+
expect(wire.data).toEqual({});
|
|
120
|
+
expect(wire.actions).toEqual({});
|
|
121
|
+
expect(wire.meta).toMatchObject({ cache_ttl_ms: 30_000, offline_policy: 'cache_only' });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('accepts headerSlot arrays and maps header camelCase', () => {
|
|
125
|
+
const wire = compileBdcToWire(
|
|
126
|
+
screen(
|
|
127
|
+
{
|
|
128
|
+
id: 'demo.home',
|
|
129
|
+
header: { brandMode: true },
|
|
130
|
+
headerSlot: [
|
|
131
|
+
{ type: 'chip', props: {}, children: [] },
|
|
132
|
+
{ type: 'chip', props: {}, children: [] },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
[],
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
expect(wire.header).toMatchObject({ brand_mode: true });
|
|
139
|
+
expect(wire.header?.slot_blocks).toHaveLength(2);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('compileBdcBlock and index re-export work', () => {
|
|
143
|
+
const block = compileBdcBlock({
|
|
144
|
+
type: '_text_node',
|
|
145
|
+
props: { value: 'x' },
|
|
146
|
+
children: [],
|
|
147
|
+
});
|
|
148
|
+
expect(block).toEqual({ id: '0', type: 'text', props: { text: 'x' } });
|
|
149
|
+
expect(viaIndex(screen({ id: 'i' }, [])).screen_id).toBe('i');
|
|
150
|
+
});
|
|
48
151
|
});
|
|
152
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
Chip,
|
|
4
|
+
ChipRow,
|
|
5
|
+
DiscoverGrid,
|
|
6
|
+
DiscoverMap,
|
|
7
|
+
DiscoverProfileCard,
|
|
8
|
+
} from './discover';
|
|
9
|
+
import { TextField } from './forms';
|
|
10
|
+
import {
|
|
11
|
+
ChatInboxCapabilities,
|
|
12
|
+
EmptyState,
|
|
13
|
+
FlashInboxCapabilities,
|
|
14
|
+
FloatingCreateMenu,
|
|
15
|
+
InboxPersonRow,
|
|
16
|
+
InboxSearchField,
|
|
17
|
+
ProfileVisitsCapabilities,
|
|
18
|
+
SegmentedTabs,
|
|
19
|
+
} from './inbox';
|
|
20
|
+
import {
|
|
21
|
+
AlbumDetail,
|
|
22
|
+
AlbumGrid,
|
|
23
|
+
ChatGroupAddMembers,
|
|
24
|
+
ChatGroupCreate,
|
|
25
|
+
ChatGroupInfo,
|
|
26
|
+
ChatGroupInvite,
|
|
27
|
+
ChatThread,
|
|
28
|
+
DiscoverBrowseLocation,
|
|
29
|
+
NewPostComposer,
|
|
30
|
+
NewStoryComposer,
|
|
31
|
+
PostComments,
|
|
32
|
+
ProfileOnboarding,
|
|
33
|
+
ProfileReport,
|
|
34
|
+
ProfileSelf,
|
|
35
|
+
ProfileSetup,
|
|
36
|
+
ProfileView,
|
|
37
|
+
SettingsHome,
|
|
38
|
+
SettingsPage,
|
|
39
|
+
SocialPostFeed,
|
|
40
|
+
SocialProfile,
|
|
41
|
+
ThoughtFeed,
|
|
42
|
+
} from './native-blocks';
|
|
43
|
+
import { ProfileCompletionBanner, StoryRail } from './social';
|
|
44
|
+
import {
|
|
45
|
+
Button,
|
|
46
|
+
Card,
|
|
47
|
+
Column,
|
|
48
|
+
InfoBanner,
|
|
49
|
+
ListItem,
|
|
50
|
+
NativeAd,
|
|
51
|
+
Row,
|
|
52
|
+
Screen,
|
|
53
|
+
Scroll,
|
|
54
|
+
SectionHeader,
|
|
55
|
+
Spacer,
|
|
56
|
+
Text,
|
|
57
|
+
ToggleRow,
|
|
58
|
+
screenProps,
|
|
59
|
+
} from './ui';
|
|
60
|
+
|
|
61
|
+
describe('bdc ui components', () => {
|
|
62
|
+
it('builds layout and leaf blocks', () => {
|
|
63
|
+
expect(Screen({ id: 'demo', children: [] })).toMatchObject({ type: 'screen' });
|
|
64
|
+
expect(Column({ gap: 8, children: [] }).type).toBe('column');
|
|
65
|
+
expect(Row({}).type).toBe('row');
|
|
66
|
+
expect(Scroll({}).type).toBe('scroll');
|
|
67
|
+
expect(Spacer({ height: 12 }).type).toBe('spacer');
|
|
68
|
+
expect(Text({ text: 'hi' }).type).toBe('text');
|
|
69
|
+
expect(Button({ actionId: 'go', label: 'Go' }).type).toBe('button');
|
|
70
|
+
expect(InfoBanner({ message: 'msg' }).type).toBe('info_banner');
|
|
71
|
+
expect(NativeAd({ placement: 'home' }).type).toBe('native_ad');
|
|
72
|
+
expect(Card({}).type).toBe('card');
|
|
73
|
+
expect(SectionHeader({ title: 'T' }).type).toBe('section_header');
|
|
74
|
+
expect(ListItem({ title: 'Item' }).type).toBe('list_item');
|
|
75
|
+
expect(ToggleRow({ label: 'On' }).type).toBe('toggle_row');
|
|
76
|
+
expect(screenProps({ id: 'x' })).toEqual({ id: 'x' });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('bdc discover/forms/social/inbox/native', () => {
|
|
81
|
+
it('builds discover and form blocks', () => {
|
|
82
|
+
expect(Chip({ label: 'All' }).type).toBe('chip');
|
|
83
|
+
expect(ChipRow({}).type).toBe('chip_row');
|
|
84
|
+
expect(DiscoverGrid({ itemsRef: 'items' }).type).toBe('discover_grid');
|
|
85
|
+
expect(DiscoverProfileCard({ profileRef: 'p' }).type).toBe('discover_profile_card');
|
|
86
|
+
expect(DiscoverMap({}).type).toBe('discover_map');
|
|
87
|
+
expect(TextField({ placeholder: 'x' }).type).toBe('text_field');
|
|
88
|
+
expect(StoryRail({ surface: 'discover' }).type).toBe('story_rail');
|
|
89
|
+
expect(ProfileCompletionBanner({}).type).toBe('profile_completion_banner');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('builds inbox blocks and normalizes floating menu actions', () => {
|
|
93
|
+
expect(SegmentedTabs({}).type).toBe('segmented_tabs');
|
|
94
|
+
expect(EmptyState({ title: 'Empty' }).type).toBe('empty_state');
|
|
95
|
+
expect(InboxPersonRow({ variant: 'chat', rowRef: 'r' }).type).toBe('inbox_person_row');
|
|
96
|
+
expect(InboxSearchField({ placeholder: 'q' }).type).toBe('inbox_search_field');
|
|
97
|
+
expect(ChatInboxCapabilities({}).type).toBe('chat_inbox_capabilities');
|
|
98
|
+
expect(FlashInboxCapabilities({}).type).toBe('flash_inbox_capabilities');
|
|
99
|
+
expect(ProfileVisitsCapabilities({}).type).toBe('profile_visits_capabilities');
|
|
100
|
+
const menu = FloatingCreateMenu({
|
|
101
|
+
id: 'fab',
|
|
102
|
+
closedLabel: '+',
|
|
103
|
+
actions: [{ id: 'a1', label: 'Post', actionId: 'create_post', tone: 'primary', icon: 'plus' }],
|
|
104
|
+
});
|
|
105
|
+
expect(menu.type).toBe('floating_create_menu');
|
|
106
|
+
expect(menu.props.actions).toEqual([
|
|
107
|
+
{ id: 'a1', label: 'Post', tone: 'primary', icon: 'plus', action_id: 'create_post' },
|
|
108
|
+
]);
|
|
109
|
+
expect(FloatingCreateMenu({ actions: [] }).props).not.toHaveProperty('id');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('builds all native hybrid blocks', () => {
|
|
113
|
+
const natives = [
|
|
114
|
+
ChatThread,
|
|
115
|
+
SocialPostFeed,
|
|
116
|
+
ThoughtFeed,
|
|
117
|
+
ProfileView,
|
|
118
|
+
ProfileSelf,
|
|
119
|
+
ProfileSetup,
|
|
120
|
+
ProfileOnboarding,
|
|
121
|
+
ProfileReport,
|
|
122
|
+
AlbumGrid,
|
|
123
|
+
AlbumDetail,
|
|
124
|
+
NewPostComposer,
|
|
125
|
+
NewStoryComposer,
|
|
126
|
+
PostComments,
|
|
127
|
+
SocialProfile,
|
|
128
|
+
SettingsHome,
|
|
129
|
+
SettingsPage,
|
|
130
|
+
ChatGroupCreate,
|
|
131
|
+
ChatGroupInfo,
|
|
132
|
+
ChatGroupAddMembers,
|
|
133
|
+
ChatGroupInvite,
|
|
134
|
+
DiscoverBrowseLocation,
|
|
135
|
+
];
|
|
136
|
+
for (const Comp of natives) {
|
|
137
|
+
const el = Comp({ id: 'n1' });
|
|
138
|
+
expect(el.children).toEqual([]);
|
|
139
|
+
expect(typeof el.type).toBe('string');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Fragment, jsx, jsxs, styleWire, type BdcElement } from './jsx-runtime';
|
|
3
|
+
|
|
4
|
+
describe('jsx-runtime', () => {
|
|
5
|
+
it('creates an element from a string type', () => {
|
|
6
|
+
const el = jsx('text', { text: 'hi' });
|
|
7
|
+
expect(el).toEqual({ type: 'text', props: { text: 'hi' }, children: [] });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('flattens null, boolean, nested arrays, and text nodes', () => {
|
|
11
|
+
const el = jsxs('column', null, null, false, true, 'hello', 42, [
|
|
12
|
+
jsx('text', { text: 'nested' }),
|
|
13
|
+
[null, jsx('spacer', {})],
|
|
14
|
+
]);
|
|
15
|
+
expect(el.children).toEqual([
|
|
16
|
+
{ type: '_text_node', props: { value: 'hello' }, children: [] },
|
|
17
|
+
{ type: '_text_node', props: { value: '42' }, children: [] },
|
|
18
|
+
{ type: 'text', props: { text: 'nested' }, children: [] },
|
|
19
|
+
{ type: 'spacer', props: {}, children: [] },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('merges children from props and rest args', () => {
|
|
24
|
+
const child: BdcElement = { type: 'text', props: { text: 'a' }, children: [] };
|
|
25
|
+
const el = jsx('row', { gap: 8, children: child }, jsx('text', { text: 'b' }));
|
|
26
|
+
expect(el.props).toEqual({ gap: 8 });
|
|
27
|
+
expect(el.children).toHaveLength(2);
|
|
28
|
+
expect(el.children[0].props).toEqual({ text: 'a' });
|
|
29
|
+
expect(el.children[1].props).toEqual({ text: 'b' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('invokes function components', () => {
|
|
33
|
+
const Comp = (props: Record<string, unknown>) => ({
|
|
34
|
+
type: 'button',
|
|
35
|
+
props: { label: props.label, children: props.children },
|
|
36
|
+
children: (props.children as BdcElement[]) ?? [],
|
|
37
|
+
});
|
|
38
|
+
const el = jsx(Comp, { label: 'Go' }, jsx('text', { text: 'inner' }));
|
|
39
|
+
expect(el.type).toBe('button');
|
|
40
|
+
expect(el.props.label).toBe('Go');
|
|
41
|
+
expect(el.children[0].type).toBe('text');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('exports Fragment and styleWire helpers', () => {
|
|
45
|
+
expect(Fragment).toBe('_fragment');
|
|
46
|
+
expect(styleWire({ padding: 'md' })).toEqual({ style: { padding: 'md' } });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('ignores non-element objects without type', () => {
|
|
50
|
+
const el = jsx('column', null, { foo: 1 });
|
|
51
|
+
expect(el.children).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { APP_ICON_NAMES, isAppIconName } from './app-icon-names';
|
|
3
|
+
import { isKnownBlockType, SDUI_BLOCK_TYPES } from './block-types';
|
|
4
|
+
import {
|
|
5
|
+
allCatalogEmojis,
|
|
6
|
+
CHAT_EMOJI_CATEGORIES,
|
|
7
|
+
DEFAULT_QUICK_REACTIONS,
|
|
8
|
+
getEmojiCategory,
|
|
9
|
+
QUICK_REACTION_SLOTS,
|
|
10
|
+
} from './chat-emoji-catalog';
|
|
11
|
+
import {
|
|
12
|
+
FLASH_FILTER_ALL_ACTION_ID,
|
|
13
|
+
FLASH_REACTION_IDS,
|
|
14
|
+
FLASH_REACTIONS,
|
|
15
|
+
flashFilterActionId,
|
|
16
|
+
getFlashReaction,
|
|
17
|
+
isFlashReactionId,
|
|
18
|
+
parseFlashInboxFilter,
|
|
19
|
+
} from './flash-reaction-catalog';
|
|
20
|
+
import {
|
|
21
|
+
COLOR_TOKENS,
|
|
22
|
+
SHAPE_TOKENS,
|
|
23
|
+
SPACING_TOKENS,
|
|
24
|
+
TYPE_TOKENS,
|
|
25
|
+
} from './style-tokens';
|
|
26
|
+
import {
|
|
27
|
+
allCatalogEmojis as viaIndexAll,
|
|
28
|
+
isAppIconName as viaIndexIsIcon,
|
|
29
|
+
isKnownBlockType as viaIndexIsBlock,
|
|
30
|
+
} from './index';
|
|
31
|
+
|
|
32
|
+
describe('block-types', () => {
|
|
33
|
+
it('lists known block types', () => {
|
|
34
|
+
expect(SDUI_BLOCK_TYPES).toContain('text');
|
|
35
|
+
expect(isKnownBlockType('button')).toBe(true);
|
|
36
|
+
expect(isKnownBlockType('unknown_block')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('app-icon-names', () => {
|
|
41
|
+
it('validates icon tokens', () => {
|
|
42
|
+
expect(APP_ICON_NAMES.length).toBeGreaterThan(10);
|
|
43
|
+
expect(isAppIconName('search')).toBe(true);
|
|
44
|
+
expect(isAppIconName('not-an-icon')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('style-tokens', () => {
|
|
49
|
+
it('exports token catalogs', () => {
|
|
50
|
+
expect(COLOR_TOKENS).toContain('primary');
|
|
51
|
+
expect(SPACING_TOKENS).toContain('md');
|
|
52
|
+
expect(SHAPE_TOKENS).toContain('cornerFull');
|
|
53
|
+
expect(TYPE_TOKENS).toContain('body');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('chat-emoji-catalog', () => {
|
|
58
|
+
it('resolves categories and flattens unique emojis', () => {
|
|
59
|
+
expect(DEFAULT_QUICK_REACTIONS).toHaveLength(QUICK_REACTION_SLOTS);
|
|
60
|
+
expect(getEmojiCategory('smileys').id).toBe('smileys');
|
|
61
|
+
expect(getEmojiCategory('spicy').id).toBe('spicy');
|
|
62
|
+
const all = allCatalogEmojis();
|
|
63
|
+
expect(all.length).toBeGreaterThan(50);
|
|
64
|
+
expect(new Set(all).size).toBe(all.length);
|
|
65
|
+
expect(CHAT_EMOJI_CATEGORIES.length).toBe(8);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('flash-reaction-catalog', () => {
|
|
70
|
+
it('looks up reactions and parses filters', () => {
|
|
71
|
+
expect(FLASH_REACTIONS).toHaveLength(FLASH_REACTION_IDS.length);
|
|
72
|
+
expect(getFlashReaction('fire').id).toBe('fire');
|
|
73
|
+
expect(getFlashReaction(null).id).toBe('flash');
|
|
74
|
+
expect(getFlashReaction('nope').id).toBe('flash');
|
|
75
|
+
expect(flashFilterActionId('heart')).toBe('flash_filter_heart');
|
|
76
|
+
expect(FLASH_FILTER_ALL_ACTION_ID).toBe('flash_filter_all');
|
|
77
|
+
expect(parseFlashInboxFilter('wave')).toBe('wave');
|
|
78
|
+
expect(parseFlashInboxFilter(undefined)).toBe('all');
|
|
79
|
+
expect(parseFlashInboxFilter('nope')).toBe('all');
|
|
80
|
+
expect(isFlashReactionId('chat')).toBe(true);
|
|
81
|
+
expect(isFlashReactionId('x')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('catalog index re-exports', () => {
|
|
86
|
+
it('re-exports helpers', () => {
|
|
87
|
+
expect(viaIndexIsBlock('text')).toBe(true);
|
|
88
|
+
expect(viaIndexIsIcon('search')).toBe(true);
|
|
89
|
+
expect(viaIndexAll().length).toBeGreaterThan(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -2,7 +2,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
formatDistanceLabel,
|
|
4
4
|
getPresenceStatus,
|
|
5
|
+
isDiscoverGridItem,
|
|
5
6
|
profileAgeFromBirthDate,
|
|
7
|
+
readDiscoverGridItems,
|
|
6
8
|
} from './discover-profile-formatters';
|
|
7
9
|
|
|
8
10
|
describe('profileAgeFromBirthDate', () => {
|
|
@@ -38,8 +40,40 @@ describe('getPresenceStatus', () => {
|
|
|
38
40
|
vi.useFakeTimers();
|
|
39
41
|
vi.setSystemTime(new Date('2026-01-01T12:00:00Z'));
|
|
40
42
|
expect(getPresenceStatus(null)).toBe('offline');
|
|
43
|
+
expect(getPresenceStatus('not-a-date')).toBe('offline');
|
|
41
44
|
expect(getPresenceStatus('2026-01-01T11:58:00Z')).toBe('online');
|
|
42
45
|
expect(getPresenceStatus('2026-01-01T06:00:00Z')).toBe('recent');
|
|
43
46
|
expect(getPresenceStatus('2025-12-01T12:00:00Z')).toBe('offline');
|
|
44
47
|
});
|
|
45
48
|
});
|
|
49
|
+
|
|
50
|
+
describe('discover grid helpers', () => {
|
|
51
|
+
it('type-guards and reads nested grid items', () => {
|
|
52
|
+
expect(isDiscoverGridItem(null)).toBe(false);
|
|
53
|
+
expect(isDiscoverGridItem({ kind: 'profile' })).toBe(true);
|
|
54
|
+
expect(isDiscoverGridItem({ kind: 'ad' })).toBe(true);
|
|
55
|
+
expect(isDiscoverGridItem({ kind: 'other' })).toBe(false);
|
|
56
|
+
|
|
57
|
+
const data = {
|
|
58
|
+
discover: {
|
|
59
|
+
items: [
|
|
60
|
+
{ kind: 'profile', id: 'p1', person: { id: 'p1' } },
|
|
61
|
+
{ kind: 'ad', id: 'a1' },
|
|
62
|
+
{ kind: 'nope' },
|
|
63
|
+
null,
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
expect(readDiscoverGridItems(data, 'discover.items')).toHaveLength(2);
|
|
68
|
+
expect(readDiscoverGridItems(data, 'discover.missing')).toEqual([]);
|
|
69
|
+
expect(readDiscoverGridItems({}, 'a.b')).toEqual([]);
|
|
70
|
+
|
|
71
|
+
vi.useFakeTimers();
|
|
72
|
+
vi.setSystemTime(new Date('2026-06-15T00:00:00Z'));
|
|
73
|
+
expect(profileAgeFromBirthDate('2000-07-01')).toBe(25);
|
|
74
|
+
expect(profileAgeFromBirthDate('2000-06-15')).toBe(26);
|
|
75
|
+
expect(profileAgeFromBirthDate('2099-01-01')).toBeNull();
|
|
76
|
+
vi.useRealTimers();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|