@csntgao/uni-base 0.1.0 → 0.1.2
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
CHANGED
|
@@ -84,6 +84,8 @@ interface UserLoginBranding {
|
|
|
84
84
|
|
|
85
85
|
type QrViewState = 'idle' | 'loading' | UserLoginQrStatus | 'error'
|
|
86
86
|
|
|
87
|
+
export const USER_LOGIN_QR_LOGIN_ENABLED = false
|
|
88
|
+
|
|
87
89
|
const EMPTY_ERRORS: FormErrors = {
|
|
88
90
|
mobile: '',
|
|
89
91
|
smsCode: '',
|
|
@@ -787,13 +789,15 @@ export class UniplatUserLogin extends LitElement {
|
|
|
787
789
|
.security-icon::before {
|
|
788
790
|
position: absolute;
|
|
789
791
|
bottom: 8px;
|
|
790
|
-
left:
|
|
792
|
+
left: 50%;
|
|
793
|
+
box-sizing: border-box;
|
|
791
794
|
width: 8px;
|
|
792
795
|
height: 6px;
|
|
793
796
|
border: 1.3px solid currentcolor;
|
|
794
797
|
border-bottom: 0;
|
|
795
798
|
border-radius: 6px 6px 0 0;
|
|
796
799
|
content: '';
|
|
800
|
+
transform: translateX(-50%);
|
|
797
801
|
}
|
|
798
802
|
|
|
799
803
|
.tab:focus-visible,
|
|
@@ -1061,9 +1065,12 @@ export class UniplatUserLogin extends LitElement {
|
|
|
1061
1065
|
}
|
|
1062
1066
|
|
|
1063
1067
|
private renderTabs() {
|
|
1068
|
+
const visibleMethods = LOGIN_METHODS.filter(
|
|
1069
|
+
({ method }) => method !== 'qr-code' || USER_LOGIN_QR_LOGIN_ENABLED,
|
|
1070
|
+
)
|
|
1064
1071
|
return html`
|
|
1065
1072
|
<div class="tabs" part="title tabs" role="tablist" aria-label="登录方式">
|
|
1066
|
-
${
|
|
1073
|
+
${visibleMethods.map(({ method, label }) => {
|
|
1067
1074
|
const available = this.isMethodAvailable(method)
|
|
1068
1075
|
return html`
|
|
1069
1076
|
<button
|
|
@@ -1424,6 +1431,7 @@ export class UniplatUserLogin extends LitElement {
|
|
|
1424
1431
|
const transport = this.config?.transport
|
|
1425
1432
|
if (method === 'verification-code') return Boolean(transport)
|
|
1426
1433
|
if (method === 'password') return typeof transport?.loginWithPassword === 'function'
|
|
1434
|
+
if (!USER_LOGIN_QR_LOGIN_ENABLED) return false
|
|
1427
1435
|
return (
|
|
1428
1436
|
typeof transport?.createQrChallenge === 'function' &&
|
|
1429
1437
|
typeof transport.pollQrChallenge === 'function'
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type UserLoginErrorDetail,
|
|
9
9
|
type UserLoginTransport,
|
|
10
10
|
} from '../src'
|
|
11
|
+
import { USER_LOGIN_QR_LOGIN_ENABLED } from '../src/user-login'
|
|
11
12
|
|
|
12
13
|
async function settle(login: UniplatUserLogin): Promise<void> {
|
|
13
14
|
await Promise.resolve()
|
|
@@ -86,14 +87,19 @@ describe('<uniplat-user-login>', () => {
|
|
|
86
87
|
expect(login.shadowRoot?.querySelector('.security-footer')?.textContent).toContain(
|
|
87
88
|
'登录凭证仅用于企业工作台认证,请勿向他人透露验证码',
|
|
88
89
|
)
|
|
90
|
+
expect(UniplatUserLogin.styles.cssText).toMatch(
|
|
91
|
+
/\.security-icon::before\s*\{[^}]*left:\s*50%;[^}]*box-sizing:\s*border-box;[^}]*transform:\s*translateX\(-50%\);/s,
|
|
92
|
+
)
|
|
89
93
|
expect(login.shadowRoot?.textContent).toContain('验证码登录')
|
|
90
|
-
expect(
|
|
94
|
+
expect(USER_LOGIN_QR_LOGIN_ENABLED).toBe(false)
|
|
95
|
+
expect(login.shadowRoot?.querySelectorAll('[role="tab"]')).toHaveLength(2)
|
|
96
|
+
expect(login.shadowRoot?.textContent).not.toContain('扫码登录')
|
|
91
97
|
expect(
|
|
92
98
|
login.shadowRoot?.querySelector<HTMLButtonElement>('[role="tab"]:first-child')?.disabled,
|
|
93
99
|
).toBe(true)
|
|
94
100
|
expect(
|
|
95
101
|
login.shadowRoot?.querySelector<HTMLButtonElement>('[role="tab"]:last-child')?.disabled,
|
|
96
|
-
).toBe(
|
|
102
|
+
).toBe(false)
|
|
97
103
|
expect(login.shadowRoot?.querySelector('#uniplat-login-image-code')).toBeNull()
|
|
98
104
|
expect(login.shadowRoot?.querySelector('.captcha-image')).toBeNull()
|
|
99
105
|
expect(login.shadowRoot?.textContent).not.toContain('图形验证码')
|
|
@@ -503,94 +509,125 @@ describe('<uniplat-user-login>', () => {
|
|
|
503
509
|
expect(passwordInput?.value).toBe('')
|
|
504
510
|
})
|
|
505
511
|
|
|
506
|
-
it('
|
|
507
|
-
vi.useFakeTimers()
|
|
512
|
+
it('hides QR login and falls back to verification login while the feature is disabled', async () => {
|
|
508
513
|
const createQrChallenge = vi.fn(async () => ({
|
|
509
|
-
challengeId: 'obviously-fake-
|
|
510
|
-
imageUrl:
|
|
511
|
-
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="120" height="120"%3E%3Crect width="120" height="120"/%3E%3C/svg%3E',
|
|
512
|
-
pollIntervalMs: 750,
|
|
514
|
+
challengeId: 'obviously-fake-hidden-challenge',
|
|
515
|
+
imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2U=',
|
|
513
516
|
}))
|
|
514
|
-
const pollQrChallenge = vi
|
|
515
|
-
.fn()
|
|
516
|
-
.mockResolvedValueOnce({ status: 'scanned' })
|
|
517
|
-
.mockResolvedValueOnce({
|
|
518
|
-
status: 'confirmed',
|
|
519
|
-
result: { applicationName: 'HrSaas' },
|
|
520
|
-
})
|
|
521
|
-
const transport = createTransport({ createQrChallenge, pollQrChallenge })
|
|
517
|
+
const pollQrChallenge = vi.fn(async () => ({ status: 'pending' as const }))
|
|
522
518
|
const login = document.createElement(USER_LOGIN_TAG)
|
|
523
|
-
const loginEvents: unknown[] = []
|
|
524
|
-
login.addEventListener('login-success', (event) => {
|
|
525
|
-
loginEvents.push((event as CustomEvent).detail)
|
|
526
|
-
})
|
|
527
519
|
login.configure({
|
|
528
|
-
transport,
|
|
520
|
+
transport: createTransport({ createQrChallenge, pollQrChallenge }),
|
|
529
521
|
applicationName: 'HrSaas',
|
|
530
522
|
initialMethod: 'qr-code',
|
|
531
|
-
qrPollIntervalMs: 500,
|
|
532
523
|
})
|
|
533
524
|
document.body.append(login)
|
|
534
525
|
await settle(login)
|
|
535
526
|
|
|
536
|
-
expect(
|
|
537
|
-
expect(login.shadowRoot?.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
expect(
|
|
543
|
-
expect(vi.getTimerCount()).toBe(1)
|
|
544
|
-
expect(login.shadowRoot?.innerHTML).not.toContain('obviously-fake-qr-challenge')
|
|
545
|
-
|
|
546
|
-
await vi.advanceTimersByTimeAsync(500)
|
|
527
|
+
expect(login.shadowRoot?.querySelectorAll('[role="tab"]')).toHaveLength(2)
|
|
528
|
+
expect(login.shadowRoot?.textContent).not.toContain('扫码登录')
|
|
529
|
+
expect(login.shadowRoot?.querySelector('.qr-panel')).toBeNull()
|
|
530
|
+
expect(
|
|
531
|
+
login.shadowRoot?.querySelector('[role="tab"][aria-selected="true"]')?.textContent,
|
|
532
|
+
).toContain('验证码登录')
|
|
533
|
+
expect(createQrChallenge).not.toHaveBeenCalled()
|
|
547
534
|
expect(pollQrChallenge).not.toHaveBeenCalled()
|
|
548
|
-
await vi.advanceTimersByTimeAsync(250)
|
|
549
|
-
await settle(login)
|
|
550
|
-
expect(login.shadowRoot?.textContent).toContain('扫码成功,请在手机上确认')
|
|
551
|
-
expect(vi.getTimerCount()).toBe(1)
|
|
552
|
-
expect(pollQrChallenge).toHaveBeenCalledWith('obviously-fake-qr-challenge', 'HrSaas')
|
|
553
|
-
|
|
554
|
-
await vi.advanceTimersByTimeAsync(750)
|
|
555
|
-
await settle(login)
|
|
556
|
-
expect(loginEvents).toEqual([{ applicationName: 'HrSaas' }])
|
|
557
|
-
expect(JSON.stringify(loginEvents)).not.toContain('obviously-fake-qr-challenge')
|
|
558
|
-
expect(vi.getTimerCount()).toBe(0)
|
|
559
535
|
})
|
|
560
536
|
|
|
561
|
-
it(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
537
|
+
it.skipIf(!USER_LOGIN_QR_LOGIN_ENABLED)(
|
|
538
|
+
'keeps QR status in one panel and clears polling after confirmation',
|
|
539
|
+
async () => {
|
|
540
|
+
vi.useFakeTimers()
|
|
541
|
+
const createQrChallenge = vi.fn(async () => ({
|
|
542
|
+
challengeId: 'obviously-fake-qr-challenge',
|
|
543
|
+
imageUrl:
|
|
544
|
+
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="120" height="120"%3E%3Crect width="120" height="120"/%3E%3C/svg%3E',
|
|
545
|
+
pollIntervalMs: 750,
|
|
546
|
+
}))
|
|
547
|
+
const pollQrChallenge = vi
|
|
548
|
+
.fn()
|
|
549
|
+
.mockResolvedValueOnce({ status: 'scanned' })
|
|
550
|
+
.mockResolvedValueOnce({
|
|
551
|
+
status: 'confirmed',
|
|
552
|
+
result: { applicationName: 'HrSaas' },
|
|
553
|
+
})
|
|
554
|
+
const transport = createTransport({ createQrChallenge, pollQrChallenge })
|
|
555
|
+
const login = document.createElement(USER_LOGIN_TAG)
|
|
556
|
+
const loginEvents: unknown[] = []
|
|
557
|
+
login.addEventListener('login-success', (event) => {
|
|
558
|
+
loginEvents.push((event as CustomEvent).detail)
|
|
559
|
+
})
|
|
560
|
+
login.configure({
|
|
561
|
+
transport,
|
|
562
|
+
applicationName: 'HrSaas',
|
|
563
|
+
initialMethod: 'qr-code',
|
|
564
|
+
qrPollIntervalMs: 500,
|
|
565
|
+
})
|
|
566
|
+
document.body.append(login)
|
|
567
|
+
await settle(login)
|
|
568
|
+
|
|
569
|
+
expect(createQrChallenge).toHaveBeenCalledWith({ applicationName: 'HrSaas' })
|
|
570
|
+
expect(login.shadowRoot?.querySelector<HTMLImageElement>('.qr-image')?.src).toContain(
|
|
571
|
+
'data:image/svg+xml',
|
|
572
|
+
)
|
|
573
|
+
expect(login.shadowRoot?.textContent).toContain('请使用微信扫描小程序码登录')
|
|
574
|
+
expect(login.shadowRoot?.textContent).toContain('未登录时先完成登录')
|
|
575
|
+
expect(login.shadowRoot?.textContent).toContain('电脑端同步登录')
|
|
576
|
+
expect(vi.getTimerCount()).toBe(1)
|
|
577
|
+
expect(login.shadowRoot?.innerHTML).not.toContain('obviously-fake-qr-challenge')
|
|
578
|
+
|
|
579
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
580
|
+
expect(pollQrChallenge).not.toHaveBeenCalled()
|
|
581
|
+
await vi.advanceTimersByTimeAsync(250)
|
|
582
|
+
await settle(login)
|
|
583
|
+
expect(login.shadowRoot?.textContent).toContain('扫码成功,请在手机上确认')
|
|
584
|
+
expect(vi.getTimerCount()).toBe(1)
|
|
585
|
+
expect(pollQrChallenge).toHaveBeenCalledWith('obviously-fake-qr-challenge', 'HrSaas')
|
|
586
|
+
|
|
587
|
+
await vi.advanceTimersByTimeAsync(750)
|
|
588
|
+
await settle(login)
|
|
589
|
+
expect(loginEvents).toEqual([{ applicationName: 'HrSaas' }])
|
|
590
|
+
expect(JSON.stringify(loginEvents)).not.toContain('obviously-fake-qr-challenge')
|
|
591
|
+
expect(vi.getTimerCount()).toBe(0)
|
|
592
|
+
},
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
it.skipIf(!USER_LOGIN_QR_LOGIN_ENABLED)(
|
|
596
|
+
'stops polling and offers a refresh after QR login is rejected',
|
|
597
|
+
async () => {
|
|
598
|
+
vi.useFakeTimers()
|
|
599
|
+
const transport = createTransport({
|
|
600
|
+
createQrChallenge: vi.fn(async () => ({
|
|
601
|
+
challengeId: 'obviously-fake-rejected-challenge',
|
|
602
|
+
imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2UtcG5n',
|
|
603
|
+
pollIntervalMs: 500,
|
|
604
|
+
})),
|
|
605
|
+
pollQrChallenge: vi.fn(async () => ({ status: 'rejected' as const })),
|
|
606
|
+
})
|
|
607
|
+
const errors: UserLoginErrorDetail[] = []
|
|
608
|
+
const login = document.createElement(USER_LOGIN_TAG)
|
|
609
|
+
login.addEventListener('error', (event) => {
|
|
610
|
+
errors.push((event as unknown as CustomEvent<UserLoginErrorDetail>).detail)
|
|
611
|
+
})
|
|
612
|
+
login.configure({ transport, applicationName: 'HrSaas', initialMethod: 'qr-code' })
|
|
613
|
+
document.body.append(login)
|
|
614
|
+
await settle(login)
|
|
615
|
+
|
|
616
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
617
|
+
await settle(login)
|
|
618
|
+
|
|
619
|
+
expect(login.shadowRoot?.textContent).toContain('登录已取消')
|
|
620
|
+
expect(login.shadowRoot?.textContent).toContain('已在手机上取消登录,请点击刷新。')
|
|
621
|
+
expect(vi.getTimerCount()).toBe(0)
|
|
622
|
+
expect(errors).toEqual([
|
|
623
|
+
{
|
|
624
|
+
code: 'QR_LOGIN_REJECTED',
|
|
625
|
+
message: '已在手机上取消登录,请点击刷新。',
|
|
626
|
+
recoverable: true,
|
|
627
|
+
},
|
|
628
|
+
])
|
|
629
|
+
},
|
|
630
|
+
)
|
|
594
631
|
|
|
595
632
|
it('maps a locked password account to a stable safe component error', async () => {
|
|
596
633
|
const transport = createTransport({
|
package/restart
ADDED