@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csntgao/uni-base",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@11.3.0",
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniplat/ai-employee-web-components",
3
- "version": "0.23.3",
3
+ "version": "0.23.5",
4
4
  "description": "Framework-independent Web Components for Uniplat account, login, application switching and AI Employee chat",
5
5
  "type": "module",
6
6
  "sideEffects": true,
@@ -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: 2px;
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
- ${LOGIN_METHODS.map(({ method, label }) => {
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(login.shadowRoot?.querySelectorAll('[role="tab"]')).toHaveLength(3)
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(true)
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('keeps QR status in one panel and clears polling after confirmation', async () => {
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-qr-challenge',
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(createQrChallenge).toHaveBeenCalledWith({ applicationName: 'HrSaas' })
537
- expect(login.shadowRoot?.querySelector<HTMLImageElement>('.qr-image')?.src).toContain(
538
- 'data:image/svg+xml',
539
- )
540
- expect(login.shadowRoot?.textContent).toContain('请使用微信扫描小程序码登录')
541
- expect(login.shadowRoot?.textContent).toContain('未登录时先完成登录')
542
- expect(login.shadowRoot?.textContent).toContain('电脑端同步登录')
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('stops polling and offers a refresh after QR login is rejected', async () => {
562
- vi.useFakeTimers()
563
- const transport = createTransport({
564
- createQrChallenge: vi.fn(async () => ({
565
- challengeId: 'obviously-fake-rejected-challenge',
566
- imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2UtcG5n',
567
- pollIntervalMs: 500,
568
- })),
569
- pollQrChallenge: vi.fn(async () => ({ status: 'rejected' as const })),
570
- })
571
- const errors: UserLoginErrorDetail[] = []
572
- const login = document.createElement(USER_LOGIN_TAG)
573
- login.addEventListener('error', (event) => {
574
- errors.push((event as unknown as CustomEvent<UserLoginErrorDetail>).detail)
575
- })
576
- login.configure({ transport, applicationName: 'HrSaas', initialMethod: 'qr-code' })
577
- document.body.append(login)
578
- await settle(login)
579
-
580
- await vi.advanceTimersByTimeAsync(500)
581
- await settle(login)
582
-
583
- expect(login.shadowRoot?.textContent).toContain('登录已取消')
584
- expect(login.shadowRoot?.textContent).toContain('已在手机上取消登录,请点击刷新。')
585
- expect(vi.getTimerCount()).toBe(0)
586
- expect(errors).toEqual([
587
- {
588
- code: 'QR_LOGIN_REJECTED',
589
- message: '已在手机上取消登录,请点击刷新。',
590
- recoverable: true,
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
@@ -0,0 +1,4 @@
1
+ #! /bin/bash
2
+ git pull
3
+ pnpm i
4
+ pnpm build