@csntgao/uni-base 0.1.1 → 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: '',
|
|
@@ -1063,9 +1065,12 @@ export class UniplatUserLogin extends LitElement {
|
|
|
1063
1065
|
}
|
|
1064
1066
|
|
|
1065
1067
|
private renderTabs() {
|
|
1068
|
+
const visibleMethods = LOGIN_METHODS.filter(
|
|
1069
|
+
({ method }) => method !== 'qr-code' || USER_LOGIN_QR_LOGIN_ENABLED,
|
|
1070
|
+
)
|
|
1066
1071
|
return html`
|
|
1067
1072
|
<div class="tabs" part="title tabs" role="tablist" aria-label="登录方式">
|
|
1068
|
-
${
|
|
1073
|
+
${visibleMethods.map(({ method, label }) => {
|
|
1069
1074
|
const available = this.isMethodAvailable(method)
|
|
1070
1075
|
return html`
|
|
1071
1076
|
<button
|
|
@@ -1426,6 +1431,7 @@ export class UniplatUserLogin extends LitElement {
|
|
|
1426
1431
|
const transport = this.config?.transport
|
|
1427
1432
|
if (method === 'verification-code') return Boolean(transport)
|
|
1428
1433
|
if (method === 'password') return typeof transport?.loginWithPassword === 'function'
|
|
1434
|
+
if (!USER_LOGIN_QR_LOGIN_ENABLED) return false
|
|
1429
1435
|
return (
|
|
1430
1436
|
typeof transport?.createQrChallenge === 'function' &&
|
|
1431
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()
|
|
@@ -90,13 +91,15 @@ describe('<uniplat-user-login>', () => {
|
|
|
90
91
|
/\.security-icon::before\s*\{[^}]*left:\s*50%;[^}]*box-sizing:\s*border-box;[^}]*transform:\s*translateX\(-50%\);/s,
|
|
91
92
|
)
|
|
92
93
|
expect(login.shadowRoot?.textContent).toContain('验证码登录')
|
|
93
|
-
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('扫码登录')
|
|
94
97
|
expect(
|
|
95
98
|
login.shadowRoot?.querySelector<HTMLButtonElement>('[role="tab"]:first-child')?.disabled,
|
|
96
99
|
).toBe(true)
|
|
97
100
|
expect(
|
|
98
101
|
login.shadowRoot?.querySelector<HTMLButtonElement>('[role="tab"]:last-child')?.disabled,
|
|
99
|
-
).toBe(
|
|
102
|
+
).toBe(false)
|
|
100
103
|
expect(login.shadowRoot?.querySelector('#uniplat-login-image-code')).toBeNull()
|
|
101
104
|
expect(login.shadowRoot?.querySelector('.captcha-image')).toBeNull()
|
|
102
105
|
expect(login.shadowRoot?.textContent).not.toContain('图形验证码')
|
|
@@ -506,94 +509,125 @@ describe('<uniplat-user-login>', () => {
|
|
|
506
509
|
expect(passwordInput?.value).toBe('')
|
|
507
510
|
})
|
|
508
511
|
|
|
509
|
-
it('
|
|
510
|
-
vi.useFakeTimers()
|
|
512
|
+
it('hides QR login and falls back to verification login while the feature is disabled', async () => {
|
|
511
513
|
const createQrChallenge = vi.fn(async () => ({
|
|
512
|
-
challengeId: 'obviously-fake-
|
|
513
|
-
imageUrl:
|
|
514
|
-
'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',
|
|
515
|
-
pollIntervalMs: 750,
|
|
514
|
+
challengeId: 'obviously-fake-hidden-challenge',
|
|
515
|
+
imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2U=',
|
|
516
516
|
}))
|
|
517
|
-
const pollQrChallenge = vi
|
|
518
|
-
.fn()
|
|
519
|
-
.mockResolvedValueOnce({ status: 'scanned' })
|
|
520
|
-
.mockResolvedValueOnce({
|
|
521
|
-
status: 'confirmed',
|
|
522
|
-
result: { applicationName: 'HrSaas' },
|
|
523
|
-
})
|
|
524
|
-
const transport = createTransport({ createQrChallenge, pollQrChallenge })
|
|
517
|
+
const pollQrChallenge = vi.fn(async () => ({ status: 'pending' as const }))
|
|
525
518
|
const login = document.createElement(USER_LOGIN_TAG)
|
|
526
|
-
const loginEvents: unknown[] = []
|
|
527
|
-
login.addEventListener('login-success', (event) => {
|
|
528
|
-
loginEvents.push((event as CustomEvent).detail)
|
|
529
|
-
})
|
|
530
519
|
login.configure({
|
|
531
|
-
transport,
|
|
520
|
+
transport: createTransport({ createQrChallenge, pollQrChallenge }),
|
|
532
521
|
applicationName: 'HrSaas',
|
|
533
522
|
initialMethod: 'qr-code',
|
|
534
|
-
qrPollIntervalMs: 500,
|
|
535
523
|
})
|
|
536
524
|
document.body.append(login)
|
|
537
525
|
await settle(login)
|
|
538
526
|
|
|
539
|
-
expect(
|
|
540
|
-
expect(login.shadowRoot?.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
expect(
|
|
546
|
-
expect(vi.getTimerCount()).toBe(1)
|
|
547
|
-
expect(login.shadowRoot?.innerHTML).not.toContain('obviously-fake-qr-challenge')
|
|
548
|
-
|
|
549
|
-
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()
|
|
550
534
|
expect(pollQrChallenge).not.toHaveBeenCalled()
|
|
551
|
-
await vi.advanceTimersByTimeAsync(250)
|
|
552
|
-
await settle(login)
|
|
553
|
-
expect(login.shadowRoot?.textContent).toContain('扫码成功,请在手机上确认')
|
|
554
|
-
expect(vi.getTimerCount()).toBe(1)
|
|
555
|
-
expect(pollQrChallenge).toHaveBeenCalledWith('obviously-fake-qr-challenge', 'HrSaas')
|
|
556
|
-
|
|
557
|
-
await vi.advanceTimersByTimeAsync(750)
|
|
558
|
-
await settle(login)
|
|
559
|
-
expect(loginEvents).toEqual([{ applicationName: 'HrSaas' }])
|
|
560
|
-
expect(JSON.stringify(loginEvents)).not.toContain('obviously-fake-qr-challenge')
|
|
561
|
-
expect(vi.getTimerCount()).toBe(0)
|
|
562
535
|
})
|
|
563
536
|
|
|
564
|
-
it(
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
)
|
|
597
631
|
|
|
598
632
|
it('maps a locked password account to a stable safe component error', async () => {
|
|
599
633
|
const transport = createTransport({
|
package/restart
ADDED