@csntgao/uni-base 0.1.3 → 0.1.5
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/AGENTS.md +14 -10
- package/CLAUDE.md +25 -14
- package/README.md +29 -3
- package/examples/vanilla/main.js +25 -6
- package/package.json +1 -1
- package/packages/web-components/package.json +1 -1
- package/packages/web-components/scripts/verify-runtime-build.mjs +1 -0
- package/packages/web-components/src/account-context.ts +29 -5
- package/packages/web-components/src/index.ts +5 -0
- package/packages/web-components/src/organization-selector.ts +435 -39
- package/packages/web-components/tests/account-context.test.ts +82 -2
- package/packages/web-components/tests/organization-selector.test.ts +226 -14
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +301 -157
|
@@ -273,7 +273,9 @@ describe('<uniplat-account-context>', () => {
|
|
|
273
273
|
await settle(context)
|
|
274
274
|
|
|
275
275
|
expect(context.shadowRoot?.textContent).toContain('工作台')
|
|
276
|
-
|
|
276
|
+
const capsuleOrganization = context.shadowRoot?.querySelector('.organization-name')
|
|
277
|
+
expect(capsuleOrganization?.textContent?.trim()).toBe('北京星河科技有限…')
|
|
278
|
+
expect(capsuleOrganization?.getAttribute('title')).toBe('北京星河科技有限公司')
|
|
277
279
|
const statusIcon = context.shadowRoot?.querySelector(`.status-icon.${cssClass}`)
|
|
278
280
|
expect(statusIcon?.getAttribute('aria-label')).toBe(label)
|
|
279
281
|
expect(statusIcon?.querySelector('svg')?.dataset.prototypeIcon).toBe(iconName)
|
|
@@ -725,6 +727,39 @@ describe('<uniplat-account-context>', () => {
|
|
|
725
727
|
)
|
|
726
728
|
})
|
|
727
729
|
|
|
730
|
+
it('falls back to the user avatar when the selected organization employee has no avatar', async () => {
|
|
731
|
+
const context = document.createElement(ACCOUNT_CONTEXT_TAG)
|
|
732
|
+
context.configure({
|
|
733
|
+
applicationName: APPLICATION_NAME,
|
|
734
|
+
displayName: '用户张伟',
|
|
735
|
+
avatarText: '用',
|
|
736
|
+
avatarUrl: 'https://static.example.com/user-avatar.png',
|
|
737
|
+
organization: {
|
|
738
|
+
id: 'enterprise-a',
|
|
739
|
+
name: '企业 A',
|
|
740
|
+
type: 'enterprise',
|
|
741
|
+
employeeDisplayName: '企业员工小张',
|
|
742
|
+
employeeAvatarText: '员',
|
|
743
|
+
verificationStatus: 'verified',
|
|
744
|
+
},
|
|
745
|
+
})
|
|
746
|
+
document.body.append(context)
|
|
747
|
+
await settle(context)
|
|
748
|
+
|
|
749
|
+
expect(context.shadowRoot?.querySelector('.display-name')?.textContent).toBe('企业员工小张')
|
|
750
|
+
expect(context.shadowRoot?.querySelector<HTMLImageElement>('.avatar-image')?.src).toBe(
|
|
751
|
+
'https://static.example.com/user-avatar.png',
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
context.shadowRoot?.querySelector<HTMLButtonElement>('.account')?.click()
|
|
755
|
+
await settle(context)
|
|
756
|
+
const menu = context.shadowRoot?.querySelector<UniplatUserMenu>('uniplat-user-menu')
|
|
757
|
+
await menu?.updateComplete
|
|
758
|
+
expect(menu?.shadowRoot?.querySelector<HTMLImageElement>('.avatar-image')?.src).toBe(
|
|
759
|
+
'https://static.example.com/user-avatar.png',
|
|
760
|
+
)
|
|
761
|
+
})
|
|
762
|
+
|
|
728
763
|
it('falls back to initials for failed avatars and rejects unsafe identity URLs', async () => {
|
|
729
764
|
const context = document.createElement(ACCOUNT_CONTEXT_TAG)
|
|
730
765
|
expect(() =>
|
|
@@ -764,6 +799,49 @@ describe('<uniplat-account-context>', () => {
|
|
|
764
799
|
expect(image?.hidden).toBe(false)
|
|
765
800
|
})
|
|
766
801
|
|
|
802
|
+
it('truncates long capsule names so the navigation width stays fixed', async () => {
|
|
803
|
+
const context = document.createElement(ACCOUNT_CONTEXT_TAG)
|
|
804
|
+
context.configure({
|
|
805
|
+
applicationName: APPLICATION_NAME,
|
|
806
|
+
displayName: '欧阳夏尔泽辉安',
|
|
807
|
+
organization: {
|
|
808
|
+
id: 'enterprise-huaqin',
|
|
809
|
+
name: '北京华勤百业技术服务有限公司',
|
|
810
|
+
type: 'enterprise',
|
|
811
|
+
applicationNames: [APPLICATION_NAME],
|
|
812
|
+
verificationStatus: 'verified',
|
|
813
|
+
},
|
|
814
|
+
})
|
|
815
|
+
document.body.append(context)
|
|
816
|
+
await settle(context)
|
|
817
|
+
|
|
818
|
+
const capsuleName = context.shadowRoot?.querySelector('.display-name')
|
|
819
|
+
const capsuleOrganization = context.shadowRoot?.querySelector('.organization-name')
|
|
820
|
+
expect(capsuleName?.textContent?.trim()).toBe('欧阳夏尔泽辉…')
|
|
821
|
+
expect(capsuleName?.getAttribute('title')).toBe('欧阳夏尔泽辉安')
|
|
822
|
+
expect(capsuleOrganization?.textContent?.trim()).toBe('北京华勤百业技术…')
|
|
823
|
+
expect(capsuleOrganization?.getAttribute('title')).toBe('北京华勤百业技术服务有限公司')
|
|
824
|
+
|
|
825
|
+
context.configure({
|
|
826
|
+
applicationName: APPLICATION_NAME,
|
|
827
|
+
displayName: '高高高',
|
|
828
|
+
organization: {
|
|
829
|
+
id: 'team-small',
|
|
830
|
+
name: '小团队',
|
|
831
|
+
type: 'team',
|
|
832
|
+
applicationNames: [APPLICATION_NAME],
|
|
833
|
+
},
|
|
834
|
+
})
|
|
835
|
+
await settle(context)
|
|
836
|
+
|
|
837
|
+
const shortName = context.shadowRoot?.querySelector('.display-name')
|
|
838
|
+
const shortOrganization = context.shadowRoot?.querySelector('.organization-name')
|
|
839
|
+
expect(shortName?.textContent?.trim()).toBe('高高高')
|
|
840
|
+
expect(shortName?.hasAttribute('title')).toBe(false)
|
|
841
|
+
expect(shortOrganization?.textContent?.trim()).toBe('小团队')
|
|
842
|
+
expect(shortOrganization?.hasAttribute('title')).toBe(false)
|
|
843
|
+
})
|
|
844
|
+
|
|
767
845
|
it('requires enterprise status, rejects raw mobile numbers and renders labels as text', async () => {
|
|
768
846
|
const context = document.createElement(ACCOUNT_CONTEXT_TAG)
|
|
769
847
|
expect(() =>
|
|
@@ -793,6 +871,8 @@ describe('<uniplat-account-context>', () => {
|
|
|
793
871
|
document.body.append(context)
|
|
794
872
|
await settle(context)
|
|
795
873
|
expect(context.shadowRoot?.querySelector('script')).toBeNull()
|
|
796
|
-
|
|
874
|
+
const capsuleName = context.shadowRoot?.querySelector('.display-name')
|
|
875
|
+
expect(capsuleName?.textContent?.trim()).toBe('<scrip…')
|
|
876
|
+
expect(capsuleName?.getAttribute('title')).toBe('<script>unsafe()</script>')
|
|
797
877
|
})
|
|
798
878
|
})
|
|
@@ -4,10 +4,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
import {
|
|
5
5
|
ORGANIZATION_SELECTOR_TAG,
|
|
6
6
|
UniplatOrganizationSelector,
|
|
7
|
+
resolveOrganizationEntryDecision,
|
|
7
8
|
type OrganizationSelectorConfig,
|
|
8
9
|
} from '../src'
|
|
9
10
|
|
|
10
11
|
const CONFIG: OrganizationSelectorConfig = {
|
|
12
|
+
displayName: '张伟',
|
|
13
|
+
maskedMobile: '138****8000',
|
|
11
14
|
selectedOrganizationId: 'enterprise-beijing',
|
|
12
15
|
rememberSelection: true,
|
|
13
16
|
organizations: [
|
|
@@ -19,6 +22,7 @@ const CONFIG: OrganizationSelectorConfig = {
|
|
|
19
22
|
role: '超管',
|
|
20
23
|
memberCount: 128,
|
|
21
24
|
lastEnteredLabel: '上次进入 2 小时前',
|
|
25
|
+
unifiedSocialCreditCode: '91110108MA01XY7788',
|
|
22
26
|
},
|
|
23
27
|
{
|
|
24
28
|
id: 'enterprise-shanghai',
|
|
@@ -58,70 +62,278 @@ async function settle(selector: UniplatOrganizationSelector): Promise<void> {
|
|
|
58
62
|
await Promise.resolve()
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
async function mount(
|
|
66
|
+
config: OrganizationSelectorConfig = CONFIG,
|
|
67
|
+
): Promise<UniplatOrganizationSelector> {
|
|
68
|
+
const selector = document.createElement(ORGANIZATION_SELECTOR_TAG)
|
|
69
|
+
selector.configure(config)
|
|
70
|
+
document.body.append(selector)
|
|
71
|
+
await settle(selector)
|
|
72
|
+
return selector
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function rowNames(selector: UniplatOrganizationSelector): string[] {
|
|
76
|
+
return [...(selector.shadowRoot?.querySelectorAll('.organization .name') ?? [])].map(
|
|
77
|
+
(node) => node.textContent?.trim() ?? '',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function typeSearch(selector: UniplatOrganizationSelector, value: string): Promise<void> {
|
|
82
|
+
const input = selector.shadowRoot?.querySelector<HTMLInputElement>('.search input')
|
|
83
|
+
if (!input) throw new Error('Missing search input')
|
|
84
|
+
input.value = value
|
|
85
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
86
|
+
await settle(selector)
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
describe('<uniplat-organization-selector>', () => {
|
|
62
90
|
afterEach(() => {
|
|
63
91
|
document.body.replaceChildren()
|
|
64
92
|
})
|
|
65
93
|
|
|
66
|
-
it('registers and renders
|
|
94
|
+
it('registers and renders the S.1 default state from the prototype', async () => {
|
|
67
95
|
expect(customElements.get(ORGANIZATION_SELECTOR_TAG)).toBe(UniplatOrganizationSelector)
|
|
68
|
-
const selector =
|
|
69
|
-
selector.configure(CONFIG)
|
|
70
|
-
document.body.append(selector)
|
|
71
|
-
await settle(selector)
|
|
96
|
+
const selector = await mount()
|
|
72
97
|
|
|
73
|
-
|
|
98
|
+
const text = selector.shadowRoot?.textContent ?? ''
|
|
99
|
+
expect(text).toContain('选择要进入的团队 / 企业')
|
|
100
|
+
expect(text).toContain('张伟 · 138****8000')
|
|
101
|
+
expect(text).toContain('你共归属 5 个组织,进入后可在导航随时切换')
|
|
102
|
+
expect(selector.shadowRoot?.querySelector('.search input')?.getAttribute('placeholder')).toBe(
|
|
103
|
+
'搜索名称,或输入统一社会信用代码',
|
|
104
|
+
)
|
|
105
|
+
expect(text).toContain('最近进入')
|
|
106
|
+
expect(text).toContain('全部组织')
|
|
74
107
|
expect(selector.shadowRoot?.querySelectorAll('.organization')).toHaveLength(5)
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
78
|
-
expect(
|
|
79
|
-
expect(
|
|
80
|
-
expect(
|
|
108
|
+
expect(text).toContain('未认证')
|
|
109
|
+
expect(text).toContain('已过期')
|
|
110
|
+
expect(text).toContain('团队')
|
|
111
|
+
expect(text).toContain('超管 · 128 人 · 上次进入 2 小时前')
|
|
112
|
+
expect(text).toContain('记住选择,下次直接进入')
|
|
113
|
+
expect(text).toContain('+ 创建 / 加入')
|
|
114
|
+
expect(text).toContain('退出登录')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('shows type filter counts and filters the list without collapsing the counts', async () => {
|
|
118
|
+
const selector = await mount()
|
|
119
|
+
const chips = [...(selector.shadowRoot?.querySelectorAll<HTMLButtonElement>('.chip') ?? [])]
|
|
120
|
+
|
|
121
|
+
expect(chips.map((chip) => chip.textContent?.trim())).toEqual(['全部 5', '企业 3', '团队 2'])
|
|
122
|
+
expect(chips[0]?.getAttribute('aria-pressed')).toBe('true')
|
|
123
|
+
|
|
124
|
+
chips[2]?.click()
|
|
125
|
+
await settle(selector)
|
|
126
|
+
|
|
127
|
+
expect(rowNames(selector)).toEqual(['星河增长项目组', '2026 校招筹备团队'])
|
|
128
|
+
expect(
|
|
129
|
+
[...(selector.shadowRoot?.querySelectorAll('.chip') ?? [])].map((chip) =>
|
|
130
|
+
chip.textContent?.trim(),
|
|
131
|
+
),
|
|
132
|
+
).toEqual(['全部 5', '企业 3', '团队 2'])
|
|
133
|
+
expect(selector.shadowRoot?.textContent).not.toContain('最近进入')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('searches by name and by unified social credit code without rendering the code', async () => {
|
|
137
|
+
const selector = await mount()
|
|
138
|
+
|
|
139
|
+
await typeSearch(selector, '上海')
|
|
140
|
+
expect(rowNames(selector)).toEqual(['星河(上海)分公司'])
|
|
141
|
+
|
|
142
|
+
await typeSearch(selector, '91110108ma01xy7788')
|
|
143
|
+
expect(rowNames(selector)).toEqual(['北京星河科技有限公司'])
|
|
144
|
+
expect(selector.shadowRoot?.textContent).not.toContain('91110108MA01XY7788')
|
|
145
|
+
|
|
146
|
+
await typeSearch(selector, '不存在的组织')
|
|
147
|
+
expect(rowNames(selector)).toEqual([])
|
|
148
|
+
expect(selector.shadowRoot?.textContent).toContain('没有匹配的团队或企业')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('groups the recently entered organization first and preselects it by default', async () => {
|
|
152
|
+
const selector = await mount({ organizations: CONFIG.organizations })
|
|
153
|
+
const groups = [...(selector.shadowRoot?.querySelectorAll('.group') ?? [])]
|
|
154
|
+
|
|
155
|
+
expect(
|
|
156
|
+
[...(groups[0]?.querySelectorAll('.name') ?? [])].map((node) => node.textContent?.trim()),
|
|
157
|
+
).toEqual(['北京星河科技有限公司'])
|
|
158
|
+
expect(
|
|
159
|
+
[...(groups[1]?.querySelectorAll('.name') ?? [])].map((node) => node.textContent?.trim()),
|
|
160
|
+
).toEqual(['星河(上海)分公司', '星河增长项目组', '已过期企业', '2026 校招筹备团队'])
|
|
161
|
+
expect(selector.shadowRoot?.querySelector('.organization.selected .name')?.textContent).toBe(
|
|
162
|
+
'北京星河科技有限公司',
|
|
163
|
+
)
|
|
164
|
+
expect(selector.shadowRoot?.querySelector('.enter')?.textContent?.trim()).toBe(
|
|
165
|
+
'进入「北京星河科技有限公司」',
|
|
166
|
+
)
|
|
81
167
|
})
|
|
82
168
|
|
|
83
169
|
it('keeps the choice local until enter and emits only ids and the remember preference', async () => {
|
|
84
170
|
const selector = document.createElement(ORGANIZATION_SELECTOR_TAG)
|
|
85
171
|
const enter = vi.fn()
|
|
86
172
|
const create = vi.fn()
|
|
173
|
+
const logout = vi.fn()
|
|
87
174
|
selector.addEventListener('organization-enter-requested', enter)
|
|
88
175
|
selector.addEventListener('organization-create-requested', create)
|
|
176
|
+
selector.addEventListener('logout-requested', logout)
|
|
89
177
|
selector.configure(CONFIG)
|
|
90
178
|
document.body.append(selector)
|
|
91
179
|
await settle(selector)
|
|
92
180
|
|
|
93
181
|
selector.shadowRoot?.querySelectorAll<HTMLButtonElement>('.organization')[2]?.click()
|
|
182
|
+
await settle(selector)
|
|
183
|
+
expect(selector.shadowRoot?.querySelector('.enter')?.textContent?.trim()).toBe(
|
|
184
|
+
'进入「星河增长项目组」',
|
|
185
|
+
)
|
|
186
|
+
|
|
94
187
|
const remember = selector.shadowRoot?.querySelector<HTMLInputElement>('.remember input')
|
|
95
188
|
if (!remember) throw new Error('Missing remember checkbox')
|
|
96
189
|
remember.checked = false
|
|
97
190
|
remember.dispatchEvent(new Event('change', { bubbles: true }))
|
|
98
191
|
selector.shadowRoot?.querySelector<HTMLButtonElement>('.enter')?.click()
|
|
99
192
|
selector.shadowRoot?.querySelector<HTMLButtonElement>('.create')?.click()
|
|
193
|
+
selector.shadowRoot?.querySelector<HTMLButtonElement>('.logout')?.click()
|
|
100
194
|
|
|
101
195
|
expect(enter.mock.calls[0]?.[0].detail).toEqual({
|
|
102
196
|
organizationId: 'team-growth',
|
|
103
197
|
rememberSelection: false,
|
|
104
198
|
})
|
|
105
|
-
|
|
199
|
+
const serialized = JSON.stringify(enter.mock.calls[0]?.[0].detail)
|
|
200
|
+
expect(serialized).not.toContain('星河')
|
|
201
|
+
expect(serialized).not.toContain('张伟')
|
|
202
|
+
expect(serialized).not.toContain('8000')
|
|
106
203
|
expect(create.mock.calls[0]?.[0].detail).toEqual({})
|
|
204
|
+
expect(logout.mock.calls[0]?.[0].detail).toEqual({})
|
|
205
|
+
expect(logout.mock.calls[0]?.[0].composed).toBe(true)
|
|
107
206
|
expect(enter.mock.calls[0]?.[0].composed).toBe(true)
|
|
108
207
|
})
|
|
109
208
|
|
|
110
|
-
it('
|
|
209
|
+
it('renders a forced modal without a close button and ignores overlay clicks and Escape', async () => {
|
|
210
|
+
const selector = await mount({ ...CONFIG, presentation: 'modal' })
|
|
211
|
+
const hostEscape = vi.fn()
|
|
212
|
+
document.addEventListener('keydown', hostEscape)
|
|
213
|
+
|
|
214
|
+
const overlay = selector.shadowRoot?.querySelector<HTMLElement>('.overlay')
|
|
215
|
+
expect(overlay).not.toBeNull()
|
|
216
|
+
expect(selector.getAttribute('data-presentation')).toBe('modal')
|
|
217
|
+
expect(selector.shadowRoot?.querySelector('.card')?.getAttribute('aria-modal')).toBe('true')
|
|
218
|
+
expect(selector.shadowRoot?.querySelectorAll('button')).not.toHaveLength(0)
|
|
219
|
+
expect(
|
|
220
|
+
[...(selector.shadowRoot?.querySelectorAll('button') ?? [])].some((button) =>
|
|
221
|
+
/关闭|close|×/i.test(`${button.className} ${button.getAttribute('part') ?? ''}`),
|
|
222
|
+
),
|
|
223
|
+
).toBe(false)
|
|
224
|
+
|
|
225
|
+
overlay?.click()
|
|
226
|
+
await settle(selector)
|
|
227
|
+
expect(selector.shadowRoot?.querySelector('.overlay')).not.toBeNull()
|
|
228
|
+
|
|
229
|
+
const escape = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
230
|
+
document.dispatchEvent(escape)
|
|
231
|
+
expect(escape.defaultPrevented).toBe(true)
|
|
232
|
+
expect(hostEscape).not.toHaveBeenCalled()
|
|
233
|
+
|
|
234
|
+
document.removeEventListener('keydown', hostEscape)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('stays inline by default and releases the Escape guard after removal', async () => {
|
|
238
|
+
const inline = await mount()
|
|
239
|
+
expect(inline.shadowRoot?.querySelector('.overlay')).toBeNull()
|
|
240
|
+
expect(inline.hasAttribute('data-presentation')).toBe(false)
|
|
241
|
+
|
|
242
|
+
const modal = await mount({ ...CONFIG, presentation: 'modal' })
|
|
243
|
+
modal.remove()
|
|
244
|
+
await settle(modal)
|
|
245
|
+
|
|
246
|
+
const hostEscape = vi.fn()
|
|
247
|
+
document.addEventListener('keydown', hostEscape)
|
|
248
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
249
|
+
expect(hostEscape).toHaveBeenCalledTimes(1)
|
|
250
|
+
document.removeEventListener('keydown', hostEscape)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('rejects duplicate ids, full mobile numbers and unknown presentations', async () => {
|
|
111
254
|
const selector = document.createElement(ORGANIZATION_SELECTOR_TAG)
|
|
112
255
|
expect(() =>
|
|
113
256
|
selector.configure({
|
|
114
257
|
organizations: [CONFIG.organizations[0]!, CONFIG.organizations[0]!],
|
|
115
258
|
}),
|
|
116
259
|
).toThrow(TypeError)
|
|
260
|
+
expect(() =>
|
|
261
|
+
selector.configure({ organizations: CONFIG.organizations, maskedMobile: '13800008000' }),
|
|
262
|
+
).toThrow(TypeError)
|
|
263
|
+
expect(() =>
|
|
264
|
+
selector.configure({
|
|
265
|
+
organizations: CONFIG.organizations,
|
|
266
|
+
presentation: 'drawer' as never,
|
|
267
|
+
}),
|
|
268
|
+
).toThrow(TypeError)
|
|
117
269
|
|
|
118
270
|
selector.configure({
|
|
119
271
|
organizations: [{ id: 'safe-id', name: '<img src=x onerror=unsafe()>', type: 'team' }],
|
|
272
|
+
displayName: '<b>张伟</b>',
|
|
120
273
|
})
|
|
121
274
|
document.body.append(selector)
|
|
122
275
|
await settle(selector)
|
|
123
276
|
|
|
124
277
|
expect(selector.shadowRoot?.querySelector('img')).toBeNull()
|
|
278
|
+
expect(selector.shadowRoot?.querySelector('b')).toBeNull()
|
|
125
279
|
expect(selector.shadowRoot?.textContent).toContain('<img src=x onerror=unsafe()>')
|
|
280
|
+
expect(selector.shadowRoot?.textContent).toContain('<b>张伟</b>')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('renders the empty state without search or filters', async () => {
|
|
284
|
+
const selector = await mount({ organizations: [] })
|
|
285
|
+
|
|
286
|
+
expect(selector.shadowRoot?.textContent).toContain('暂无可进入的团队或企业')
|
|
287
|
+
expect(selector.shadowRoot?.querySelector('.search')).toBeNull()
|
|
288
|
+
expect(selector.shadowRoot?.querySelector('.chip')).toBeNull()
|
|
289
|
+
expect(selector.shadowRoot?.querySelector<HTMLButtonElement>('.enter')?.disabled).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('resolveOrganizationEntryDecision()', () => {
|
|
294
|
+
it('sends users without memberships to the personal state', () => {
|
|
295
|
+
expect(resolveOrganizationEntryDecision({ organizations: [] })).toEqual({ kind: 'personal' })
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('enters directly with a single membership even when nothing is remembered', () => {
|
|
299
|
+
expect(resolveOrganizationEntryDecision({ organizations: [CONFIG.organizations[2]!] })).toEqual(
|
|
300
|
+
{ kind: 'auto-enter', organizationId: 'team-growth' },
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('skips the prompt when the remembered organization is still available', () => {
|
|
305
|
+
expect(
|
|
306
|
+
resolveOrganizationEntryDecision({
|
|
307
|
+
organizations: CONFIG.organizations,
|
|
308
|
+
rememberedOrganizationId: 'team-campus',
|
|
309
|
+
}),
|
|
310
|
+
).toEqual({ kind: 'auto-enter', organizationId: 'team-campus' })
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('prompts with the recently entered organization for two or more memberships', () => {
|
|
314
|
+
expect(resolveOrganizationEntryDecision({ organizations: CONFIG.organizations })).toEqual({
|
|
315
|
+
kind: 'prompt',
|
|
316
|
+
selectedOrganizationId: 'enterprise-beijing',
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('prompts again when the remembered organization is no longer available', () => {
|
|
321
|
+
expect(
|
|
322
|
+
resolveOrganizationEntryDecision({
|
|
323
|
+
organizations: CONFIG.organizations,
|
|
324
|
+
rememberedOrganizationId: 'enterprise-removed',
|
|
325
|
+
}),
|
|
326
|
+
).toEqual({ kind: 'prompt', selectedOrganizationId: 'enterprise-beijing' })
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('rejects malformed input instead of guessing an entry', () => {
|
|
330
|
+
expect(() => resolveOrganizationEntryDecision({ organizations: undefined as never })).toThrow(
|
|
331
|
+
TypeError,
|
|
332
|
+
)
|
|
333
|
+
expect(() =>
|
|
334
|
+
resolveOrganizationEntryDecision({
|
|
335
|
+
organizations: [CONFIG.organizations[0]!, CONFIG.organizations[0]!],
|
|
336
|
+
}),
|
|
337
|
+
).toThrow(TypeError)
|
|
126
338
|
})
|
|
127
339
|
})
|