@block-auth.io/angular 0.1.0
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/.turbo/turbo-build.log +37 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +82 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +10 -0
- package/ng-package.json +6 -0
- package/package.json +50 -0
- package/scripts/css-to-ts.mjs +14 -0
- package/src/lib/__tests__/integration.test.ts +45 -0
- package/src/lib/block-auth.service.test.ts +69 -0
- package/src/lib/block-auth.service.ts +81 -0
- package/src/lib/components/btns/btn-base-provider.component.test.ts +22 -0
- package/src/lib/components/btns/btn-base-provider.component.ts +55 -0
- package/src/lib/components/btns/btn-block-auth.component.test.ts +79 -0
- package/src/lib/components/btns/btn-block-auth.component.ts +104 -0
- package/src/lib/components/btns/btn-gmail.component.test.ts +16 -0
- package/src/lib/components/btns/btn-gmail.component.ts +41 -0
- package/src/lib/components/btns/btn-google-base.component.test.ts +16 -0
- package/src/lib/components/btns/btn-google-base.component.ts +73 -0
- package/src/lib/components/btns/btn-metamask.component.test.ts +20 -0
- package/src/lib/components/btns/btn-metamask.component.ts +98 -0
- package/src/lib/components/btns/btn-microsoft.component.test.ts +16 -0
- package/src/lib/components/btns/btn-microsoft.component.ts +77 -0
- package/src/lib/components/btns/btn-phantom.component.test.ts +20 -0
- package/src/lib/components/btns/btn-phantom.component.ts +93 -0
- package/src/lib/components/btns/btn-signin.component.test.ts +55 -0
- package/src/lib/components/btns/btn-signin.component.ts +108 -0
- package/src/lib/components/btns/btn-wallet-connect.component.test.ts +20 -0
- package/src/lib/components/btns/btn-wallet-connect.component.ts +109 -0
- package/src/lib/components/btns/index.ts +11 -0
- package/src/lib/components/contents/content-signin.component.test.ts +33 -0
- package/src/lib/components/contents/content-signin.component.ts +68 -0
- package/src/lib/components/contents/content-signup.component.test.ts +78 -0
- package/src/lib/components/contents/content-signup.component.ts +225 -0
- package/src/lib/components/dds/change-account.component.test.ts +66 -0
- package/src/lib/components/dds/change-account.component.ts +112 -0
- package/src/lib/components/dds/dropdown-profile-btn.component.test.ts +66 -0
- package/src/lib/components/dds/dropdown-profile-btn.component.ts +185 -0
- package/src/lib/components/dds/dropdown-profile-item.component.test.ts +75 -0
- package/src/lib/components/dds/dropdown-profile-item.component.ts +74 -0
- package/src/lib/components/dds/dropdown-profile.component.test.ts +98 -0
- package/src/lib/components/dds/dropdown-profile.component.ts +258 -0
- package/src/lib/components/flow-block-auth.component.test.ts +93 -0
- package/src/lib/components/flow-block-auth.component.ts +226 -0
- package/src/lib/components/modals/modal-overlay.component.test.ts +81 -0
- package/src/lib/components/modals/modal-overlay.component.ts +43 -0
- package/src/lib/components/modals/modal-restoration-expired.component.test.ts +71 -0
- package/src/lib/components/modals/modal-restoration-expired.component.ts +83 -0
- package/src/lib/components/modals/modal-restore.component.test.ts +74 -0
- package/src/lib/components/modals/modal-restore.component.ts +104 -0
- package/src/lib/components/modals/modal-signin.component.test.ts +66 -0
- package/src/lib/components/modals/modal-signin.component.ts +44 -0
- package/src/lib/components/modals/modal-signup.component.test.ts +56 -0
- package/src/lib/components/modals/modal-signup.component.ts +40 -0
- package/src/lib/components/sections/footer-section.component.test.ts +19 -0
- package/src/lib/components/sections/footer-section.component.ts +49 -0
- package/src/lib/components/sections/internals/providers-buttons.component.test.ts +27 -0
- package/src/lib/components/sections/internals/providers-buttons.component.ts +290 -0
- package/src/lib/components/sections/internals/providers-filters.component.test.ts +25 -0
- package/src/lib/components/sections/internals/providers-filters.component.ts +139 -0
- package/src/lib/components/sections/internals/selected-button.component.test.ts +36 -0
- package/src/lib/components/sections/internals/selected-button.component.ts +92 -0
- package/src/lib/components/sections/login-section.component.test.ts +49 -0
- package/src/lib/components/sections/login-section.component.ts +120 -0
- package/src/lib/components/sections/providers-section.component.test.ts +22 -0
- package/src/lib/components/sections/providers-section.component.ts +187 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.test.ts +14 -0
- package/src/lib/components/txns/internals/permissions-required-modal.component.ts +35 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.test.ts +72 -0
- package/src/lib/components/txns/internals/solicited-data-modal.component.ts +187 -0
- package/src/lib/components/txns/internals/txn-error.component.test.ts +100 -0
- package/src/lib/components/txns/internals/txn-error.component.ts +121 -0
- package/src/lib/components/txns/internals/txn-type.component.test.ts +52 -0
- package/src/lib/components/txns/internals/txn-type.component.ts +51 -0
- package/src/lib/components/txns/internals/txn.component.test.ts +188 -0
- package/src/lib/components/txns/internals/txn.component.ts +298 -0
- package/src/lib/components/txns/txns-overview.component.test.ts +107 -0
- package/src/lib/components/txns/txns-overview.component.ts +229 -0
- package/src/lib/icons/ai-outline-disconnect.component.ts +21 -0
- package/src/lib/icons/bi-logo-gmail.component.ts +21 -0
- package/src/lib/icons/cg-profile.component.ts +31 -0
- package/src/lib/icons/check-icon.component.ts +27 -0
- package/src/lib/icons/ci-clock2.component.ts +31 -0
- package/src/lib/icons/fa-arrow-right.component.ts +21 -0
- package/src/lib/icons/fa-times.component.ts +21 -0
- package/src/lib/icons/fa-undo.component.ts +21 -0
- package/src/lib/icons/icons.test.ts +25 -0
- package/src/lib/icons/index.ts +22 -0
- package/src/lib/icons/io-ios-arrow-back.component.ts +21 -0
- package/src/lib/icons/io-ios-arrow-forward.component.ts +21 -0
- package/src/lib/icons/io-md-arrow-back.component.ts +19 -0
- package/src/lib/icons/io-md-arrow-forward.component.ts +19 -0
- package/src/lib/icons/pi-warning-fill.component.ts +21 -0
- package/src/lib/icons/tb-arrows-exchange2.component.ts +30 -0
- package/src/lib/icons/tb-copy-check-filled.component.ts +21 -0
- package/src/lib/icons/tb-copy.component.ts +34 -0
- package/src/lib/icons/ti-vendor-microsoft.component.ts +21 -0
- package/src/lib/inject-store.test.ts +26 -0
- package/src/lib/inject-store.ts +16 -0
- package/src/lib/primitives/arrow-icon.component.test.ts +12 -0
- package/src/lib/primitives/arrow-icon.component.ts +38 -0
- package/src/lib/primitives/btn-arrow-dir.component.test.ts +22 -0
- package/src/lib/primitives/btn-arrow-dir.component.ts +65 -0
- package/src/lib/primitives/btn-base.component.test.ts +38 -0
- package/src/lib/primitives/btn-base.component.ts +140 -0
- package/src/lib/primitives/divider-base.component.test.ts +12 -0
- package/src/lib/primitives/divider-base.component.ts +74 -0
- package/src/lib/primitives/dropdown-arrow.component.test.ts +12 -0
- package/src/lib/primitives/dropdown-arrow.component.ts +46 -0
- package/src/lib/primitives/dropdown-base.component.test.ts +49 -0
- package/src/lib/primitives/dropdown-base.component.ts +329 -0
- package/src/lib/primitives/helpers.ts +151 -0
- package/src/lib/primitives/horizontal-loader.component.test.ts +19 -0
- package/src/lib/primitives/horizontal-loader.component.ts +70 -0
- package/src/lib/primitives/icon-base.component.test.ts +19 -0
- package/src/lib/primitives/icon-base.component.ts +58 -0
- package/src/lib/primitives/img-loader.component.test.ts +12 -0
- package/src/lib/primitives/img-loader.component.ts +118 -0
- package/src/lib/primitives/index.ts +29 -0
- package/src/lib/primitives/input-base.component.test.ts +20 -0
- package/src/lib/primitives/input-base.component.ts +210 -0
- package/src/lib/primitives/lifetime-slider-base.component.test.ts +88 -0
- package/src/lib/primitives/lifetime-slider-base.component.ts +75 -0
- package/src/lib/primitives/oval-spinner.component.test.ts +9 -0
- package/src/lib/primitives/oval-spinner.component.ts +158 -0
- package/src/lib/primitives/pickers/dropdown-picker-animated-base.component.ts +220 -0
- package/src/lib/primitives/pickers/picker-button.component.ts +74 -0
- package/src/lib/primitives/pickers/time-picker-dropdown-selector.component.ts +188 -0
- package/src/lib/primitives/pickers/time-picker.component.ts +124 -0
- package/src/lib/primitives/pickers/time-unit-selector.component.ts +82 -0
- package/src/lib/primitives/progressbar.component.test.ts +18 -0
- package/src/lib/primitives/progressbar.component.ts +93 -0
- package/src/lib/primitives/providersStyles.ts +268 -0
- package/src/lib/primitives/slider-base.component.ts +56 -0
- package/src/lib/provide-block-auth.test.ts +43 -0
- package/src/lib/provide-block-auth.ts +43 -0
- package/src/lib/runtime/block-auth-runtime.directive.test.ts +89 -0
- package/src/lib/runtime/block-auth-runtime.directive.ts +68 -0
- package/src/lib/teleport-body.directive.test.ts +29 -0
- package/src/lib/teleport-body.directive.ts +21 -0
- package/src/lib/testing.ts +15 -0
- package/src/lib/transitions.css +23 -0
- package/src/public-api.ts +85 -0
- package/src/vitest.setup.ts +8 -0
- package/tailwind.config.cjs +6 -0
- package/tsconfig.json +15 -0
- package/tsconfig.lib.json +9 -0
- package/vite.config.mts +44 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { injectBlockAuthClient } from '../../../provide-block-auth';
|
|
3
|
+
import { injectStore } from '../../../inject-store';
|
|
4
|
+
import { BtnBlockAuthComponent } from '../../btns/btn-block-auth.component';
|
|
5
|
+
import { BtnGmailComponent } from '../../btns/btn-gmail.component';
|
|
6
|
+
import { BtnMicrosoftComponent } from '../../btns/btn-microsoft.component';
|
|
7
|
+
import { BtnMetamaskComponent } from '../../btns/btn-metamask.component';
|
|
8
|
+
import { BtnPhantomComponent } from '../../btns/btn-phantom.component';
|
|
9
|
+
import { BtnWalletConnectComponent } from '../../btns/btn-wallet-connect.component';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Renderiza el botón de conexión del provider elegido. La rama
|
|
13
|
+
* `isWeb3Service` (cualquier provider habilitado no built-in) renderiza
|
|
14
|
+
* `<ba-btn-wallet-connect>` con el `walletService` seleccionado. `uiConfig`
|
|
15
|
+
* se lee reactivamente del store `$ui` del core.
|
|
16
|
+
*
|
|
17
|
+
* NOTA de idioma Angular: cuando `btnsConnectTxt` es falsy, el original de
|
|
18
|
+
* react/vue deja que cada botón use su propio label por defecto pasando
|
|
19
|
+
* `undefined`. En Angular una binding `[text]="undefined"` PISA el default de
|
|
20
|
+
* `input()` (a diferencia de Vue/React), así que se pasa como fallback el
|
|
21
|
+
* label por defecto del propio botón para conservar la paridad.
|
|
22
|
+
*/
|
|
23
|
+
const BUILT_IN = ['blockauth', 'google', 'microsoft', 'metamask', 'phantom'];
|
|
24
|
+
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'ba-selected-button',
|
|
27
|
+
standalone: true,
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
imports: [
|
|
30
|
+
BtnBlockAuthComponent,
|
|
31
|
+
BtnGmailComponent,
|
|
32
|
+
BtnMicrosoftComponent,
|
|
33
|
+
BtnMetamaskComponent,
|
|
34
|
+
BtnPhantomComponent,
|
|
35
|
+
BtnWalletConnectComponent,
|
|
36
|
+
],
|
|
37
|
+
template: `
|
|
38
|
+
@if (selectedService() === 'blockauth') {
|
|
39
|
+
<ba-btn-block-auth [disabled]="disabled()" [isEditable]="isEditable()" />
|
|
40
|
+
}
|
|
41
|
+
@if (selectedService() === 'google') {
|
|
42
|
+
<ba-btn-gmail [text]="connectText() ?? 'Gmail'" [disabled]="disabled()" [isEditable]="isEditable()" />
|
|
43
|
+
}
|
|
44
|
+
@if (selectedService() === 'microsoft') {
|
|
45
|
+
<ba-btn-microsoft [text]="connectText() ?? 'Microsoft'" [disabled]="disabled()" [isEditable]="isEditable()" />
|
|
46
|
+
}
|
|
47
|
+
@if (selectedService() === 'metamask') {
|
|
48
|
+
<ba-btn-metamask
|
|
49
|
+
[text]="connectText() ?? 'MetaMask'"
|
|
50
|
+
[disabled]="disabled()"
|
|
51
|
+
[isTextInsteadOfAccount]="true"
|
|
52
|
+
[isEditable]="isEditable()"
|
|
53
|
+
/>
|
|
54
|
+
}
|
|
55
|
+
@if (selectedService() === 'phantom') {
|
|
56
|
+
<ba-btn-phantom
|
|
57
|
+
[text]="connectText() ?? 'Phantom'"
|
|
58
|
+
[disabled]="disabled()"
|
|
59
|
+
[isTextInsteadOfAccount]="true"
|
|
60
|
+
[isEditable]="isEditable()"
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
@if (isWeb3Service()) {
|
|
64
|
+
<ba-btn-wallet-connect
|
|
65
|
+
[text]="connectText() ?? 'Connect Wallet'"
|
|
66
|
+
[disabled]="disabled()"
|
|
67
|
+
[isTextInsteadOfAccount]="true"
|
|
68
|
+
[isEditable]="isEditable()"
|
|
69
|
+
[walletService]="selectedService()"
|
|
70
|
+
/>
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
})
|
|
74
|
+
export class SelectedButtonComponent {
|
|
75
|
+
selectedService = input('');
|
|
76
|
+
btnsConnectTxt = input('');
|
|
77
|
+
showImgLoader = input(true);
|
|
78
|
+
isEditable = input(false);
|
|
79
|
+
|
|
80
|
+
private readonly client = injectBlockAuthClient();
|
|
81
|
+
private readonly ui = injectStore(this.client.state.$ui);
|
|
82
|
+
|
|
83
|
+
// Solo se pasa el texto de conexión cuando existe; si no, `undefined` para
|
|
84
|
+
// que el fallback del template use el label por defecto de cada botón.
|
|
85
|
+
protected connectText = computed<string | undefined>(() => this.btnsConnectTxt() || undefined);
|
|
86
|
+
protected disabled = computed(() => !this.showImgLoader());
|
|
87
|
+
protected isWeb3Service = computed(() => {
|
|
88
|
+
const config = (this.ui() as Record<string, any> | null)?.uiConfig?.config;
|
|
89
|
+
const svc = this.selectedService();
|
|
90
|
+
return !!config && !!config[svc] && !!config[svc].enabled && !BUILT_IN.includes(svc);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen } from '@testing-library/angular';
|
|
4
|
+
import { map } from 'nanostores';
|
|
5
|
+
import { LoginSectionComponent } from './login-section.component';
|
|
6
|
+
import { provideBlockAuthClientForTest } from '../../testing';
|
|
7
|
+
|
|
8
|
+
vi.mock('@wagmi/core', () => ({
|
|
9
|
+
createConfig: vi.fn(() => ({ connectors: [] })),
|
|
10
|
+
connect: vi.fn(), disconnect: vi.fn(), getAccount: vi.fn(() => ({})),
|
|
11
|
+
signMessage: vi.fn(), watchAccount: vi.fn(() => () => undefined), http: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('@wagmi/connectors', () => ({
|
|
14
|
+
injected: vi.fn(() => ({ id: 'injected' })), walletConnect: vi.fn(() => ({ id: 'walletConnect' })),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function makeClient() {
|
|
18
|
+
return {
|
|
19
|
+
web3: { signup: vi.fn() },
|
|
20
|
+
state: { $ui: map({}), $wallet: map({}), $ws: map({}) },
|
|
21
|
+
} as any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Port de packages/react/src/components/sections/LoginSection.test.tsx.
|
|
25
|
+
describe('LoginSection', () => {
|
|
26
|
+
it('renderiza la seccion de login', async () => {
|
|
27
|
+
@Component({
|
|
28
|
+
standalone: true,
|
|
29
|
+
imports: [LoginSectionComponent],
|
|
30
|
+
template: `<ba-login-section selectedService="metamask" createAccountTxt="Crear cuenta ahora" />`,
|
|
31
|
+
})
|
|
32
|
+
class HostComponent {}
|
|
33
|
+
await render(HostComponent, { providers: [provideBlockAuthClientForTest(makeClient())] });
|
|
34
|
+
expect(screen.getByTestId('login-section')).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('en la rama conectada (showImgLoader=false) renderiza el <ba-dropdown-profile> real (A10)', async () => {
|
|
38
|
+
@Component({
|
|
39
|
+
standalone: true,
|
|
40
|
+
imports: [LoginSectionComponent],
|
|
41
|
+
template: `<ba-login-section selectedService="blockauth" [showImgLoader]="false" [isServiceConnected]="true" />`,
|
|
42
|
+
})
|
|
43
|
+
class HostComponent {}
|
|
44
|
+
await render(HostComponent, { providers: [provideBlockAuthClientForTest(makeClient())] });
|
|
45
|
+
expect(screen.getByTestId('login-section')).toBeDefined();
|
|
46
|
+
expect(screen.getByTestId('dropdown-profile')).toBeDefined();
|
|
47
|
+
expect(screen.queryByTestId('dropdown-profile-placeholder')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { providersStyles } from '../../primitives/providersStyles';
|
|
3
|
+
import { ImgLoaderComponent } from '../../primitives/img-loader.component';
|
|
4
|
+
import { ArrowIconComponent } from '../../primitives/arrow-icon.component';
|
|
5
|
+
import { SelectedButtonComponent } from './internals/selected-button.component';
|
|
6
|
+
import { BtnSigninComponent } from '../btns/btn-signin.component';
|
|
7
|
+
import { DropdownProfileComponent } from '../dds/dropdown-profile.component';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Columna derecha de la card de signup. Dos estados, según `showImgLoader`
|
|
11
|
+
* (= no conectado al servicio seleccionado):
|
|
12
|
+
* - NO conectado -> logo del provider (`<ba-img-loader>`) + botón de conexión
|
|
13
|
+
* (`<ba-selected-button>`) + descripción.
|
|
14
|
+
* - conectado -> el `<DropdownProfile>` de cuenta + separador "Completa el
|
|
15
|
+
* último paso" + la fila `<ba-btn-signin>` "Iniciar sesión" flanqueada por
|
|
16
|
+
* `<ba-arrow-icon>`.
|
|
17
|
+
*
|
|
18
|
+
* NOTA: `containerRef` existe en la interfaz react pero nunca se usa en el
|
|
19
|
+
* cuerpo (prop muerta) — se omite, igual que en el port Vue.
|
|
20
|
+
*
|
|
21
|
+
* A10: el placeholder de A9 (rama conectada) ahora renderiza el
|
|
22
|
+
* `<ba-dropdown-profile>` real.
|
|
23
|
+
*/
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'ba-login-section',
|
|
26
|
+
standalone: true,
|
|
27
|
+
// Hijo directo del grid de content-signup: el host debe ser transparente al
|
|
28
|
+
// layout para que el contenido interno sea el grid item real (ver
|
|
29
|
+
// providers-section para el detalle). Sin esto el layout del modal se solapa.
|
|
30
|
+
host: { style: 'display: contents' },
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
imports: [
|
|
33
|
+
ImgLoaderComponent,
|
|
34
|
+
ArrowIconComponent,
|
|
35
|
+
SelectedButtonComponent,
|
|
36
|
+
BtnSigninComponent,
|
|
37
|
+
DropdownProfileComponent,
|
|
38
|
+
],
|
|
39
|
+
template: `
|
|
40
|
+
<section
|
|
41
|
+
data-testid="login-section"
|
|
42
|
+
[attr.data-service]="selectedService()"
|
|
43
|
+
class="bas-relative bas-box-border bas-flex bas-h-[230px] bas-w-full bas-flex-col bas-items-center bas-justify-between bas-gap-2 bas-pt-4 bas-px-2 lg:bas-px-6"
|
|
44
|
+
>
|
|
45
|
+
@if (showImgLoader()) {
|
|
46
|
+
<ba-img-loader
|
|
47
|
+
loaderId="signup-provider-loader"
|
|
48
|
+
[imgSrc]="isWeb3ProviderSelected() && imgQR() ? imgQR() : style().imgSrc"
|
|
49
|
+
[imgAlt]="'Img of ' + selectedService() + ' provider'"
|
|
50
|
+
[imgBgColor]="style().imgBgColor"
|
|
51
|
+
[loaderColor]="style().loaderColor"
|
|
52
|
+
[scaleImgWhenHovered]="!isWeb3ProviderSelected()"
|
|
53
|
+
[size]="isWeb3ProviderSelected() ? 'medium' : 'small'"
|
|
54
|
+
/>
|
|
55
|
+
} @else {
|
|
56
|
+
<ba-dropdown-profile [isInsideModal]="true" dropdownBtnSize="small" />
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
<div class="bas-relative bas-box-border bas-flex bas-h-auto bas-w-full bas-flex-col bas-gap-4 bas-overflow-hidden bas-py-1">
|
|
60
|
+
@if (showImgLoader()) {
|
|
61
|
+
<!-- BOTON DE CONEXION DEL PROVIDER -->
|
|
62
|
+
<div class="bas-flex bas-h-auto bas-w-auto bas-justify-center">
|
|
63
|
+
<ba-selected-button
|
|
64
|
+
[selectedService]="selectedService()"
|
|
65
|
+
[btnsConnectTxt]="connectTxt()"
|
|
66
|
+
[showImgLoader]="showImgLoader()"
|
|
67
|
+
[isEditable]="isEditable()"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<!-- DESCRIPCION -->
|
|
71
|
+
<p [class]="'bas-m-0 bas-mx-auto bas-text-center bas-text-gray-900 ' + descriptionTextClassNames()">
|
|
72
|
+
{{ titleTxt() }} {{ descriptionTxt() }}
|
|
73
|
+
</p>
|
|
74
|
+
} @else {
|
|
75
|
+
<!-- CONECTADO: "Completa el último paso" + fila de Log In -->
|
|
76
|
+
<div class="bas-flex bas-w-full bas-flex-col bas-items-center bas-gap-4 bas-pt-2">
|
|
77
|
+
<div class="bas-flex bas-w-full bas-items-center bas-justify-center bas-gap-3">
|
|
78
|
+
<span class="bas-h-px bas-flex-1 bas-bg-[var(--bal-primary-300)]"></span>
|
|
79
|
+
<p class="bas-m-0 bas-text-center bas-text-[var(--bal-primary-600)]">{{ lastStepTxt() }}</p>
|
|
80
|
+
<span class="bas-h-px bas-flex-1 bas-bg-[var(--bal-primary-300)]"></span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="bas-flex bas-h-fit bas-w-full bas-flex-row bas-items-center bas-justify-center bas-gap-4">
|
|
83
|
+
<ba-arrow-icon direction="right" />
|
|
84
|
+
<ba-btn-signin [text]="loginTxt()" [isDisabled]="!isServiceConnected()" [isEditable]="isEditable()" />
|
|
85
|
+
<ba-arrow-icon direction="left" />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
}
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
`,
|
|
92
|
+
})
|
|
93
|
+
export class LoginSectionComponent {
|
|
94
|
+
imgQR = input('');
|
|
95
|
+
showImgLoader = input(true);
|
|
96
|
+
selectedService = input('blockauth');
|
|
97
|
+
isServiceConnected = input(false);
|
|
98
|
+
isEditable = input(false);
|
|
99
|
+
device = input<'desktop' | 'tablet' | 'phone'>('desktop');
|
|
100
|
+
titleTxt = input('');
|
|
101
|
+
descriptionTxt = input('Regístrate en segundos y olvídate de las contraseñas.');
|
|
102
|
+
/** Etiqueta localizada del botón de conexión del provider. */
|
|
103
|
+
btnsConnectTxt = input('');
|
|
104
|
+
/** @deprecated usar `btnsConnectTxt`. */
|
|
105
|
+
createAccountTxt = input('');
|
|
106
|
+
/** Etiqueta localizada de "Log In" (estado conectado). */
|
|
107
|
+
loginTxt = input('Iniciar sesión');
|
|
108
|
+
/** Etiqueta localizada de "Completa el último paso" (estado conectado). */
|
|
109
|
+
lastStepTxt = input('Completa el último paso');
|
|
110
|
+
|
|
111
|
+
protected isWeb3ProviderSelected = computed(
|
|
112
|
+
() => this.selectedService() === 'metamask' || this.selectedService() === 'phantom',
|
|
113
|
+
);
|
|
114
|
+
protected style = computed(() => providersStyles[this.selectedService()] ?? providersStyles['blockauth']);
|
|
115
|
+
protected connectTxt = computed(() => this.btnsConnectTxt() || this.createAccountTxt());
|
|
116
|
+
protected descriptionTextClassNames = computed(() => {
|
|
117
|
+
if (!this.isEditable()) return 'bas-text-xs bas-font-light lg:bas-text-base lg:bas-font-thin';
|
|
118
|
+
return this.device() === 'desktop' ? 'bas-text-base bas-font-thin' : 'bas-text-xs bas-font-light';
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen } from '@testing-library/angular';
|
|
4
|
+
import { ProvidersSectionComponent } from './providers-section.component';
|
|
5
|
+
|
|
6
|
+
// Port de packages/react/src/components/sections/ProvidersSection.test.tsx.
|
|
7
|
+
// Usa `$locale` del core (singleton) para i18n; sin dependencias del client.
|
|
8
|
+
describe('ProvidersSection', () => {
|
|
9
|
+
it('renderiza la seccion de proveedores', async () => {
|
|
10
|
+
@Component({
|
|
11
|
+
standalone: true,
|
|
12
|
+
imports: [ProvidersSectionComponent],
|
|
13
|
+
template: `<ba-providers-section [showedProviders]="providers" />`,
|
|
14
|
+
})
|
|
15
|
+
class HostComponent {
|
|
16
|
+
providers = ['metamask', 'phantom'];
|
|
17
|
+
}
|
|
18
|
+
await render(HostComponent);
|
|
19
|
+
expect(screen.getByTestId('providers-section')).toBeDefined();
|
|
20
|
+
expect(screen.getByTestId('providers-buttons')).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
DestroyRef,
|
|
5
|
+
PLATFORM_ID,
|
|
6
|
+
computed,
|
|
7
|
+
inject,
|
|
8
|
+
input,
|
|
9
|
+
output,
|
|
10
|
+
signal,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
13
|
+
import { $locale, t as translate, type TextKey } from '@block-auth.io/core';
|
|
14
|
+
import { injectStore } from '../../inject-store';
|
|
15
|
+
import {
|
|
16
|
+
ProvidersFiltersComponent,
|
|
17
|
+
makeWebTypeItems,
|
|
18
|
+
type WebTypeItem,
|
|
19
|
+
} from './internals/providers-filters.component';
|
|
20
|
+
import { ProvidersButtonsComponent } from './internals/providers-buttons.component';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Columna izquierda de la card de signup: cabecera "Conectar con" (logo +
|
|
24
|
+
* título), filtros de búsqueda/web-type y grid de proveedores. Estado local de
|
|
25
|
+
* búsqueda/filtro cableado hacia abajo y expuesto al padre vía outputs
|
|
26
|
+
* (`selectedServiceChange`/`changeWebType`/`changeProviderSearch`).
|
|
27
|
+
*
|
|
28
|
+
* `useMobile(660)` -> señal `isMobile` interna, SSR-safe (queda en `false`
|
|
29
|
+
* hasta montar en cliente) y reactiva a `resize`. El efecto de medición de
|
|
30
|
+
* scroll se internaliza en `<ba-providers-buttons>` (ver su cabecera). i18n vía
|
|
31
|
+
* `$locale` del core (mismo patrón que BtnBlockAuth).
|
|
32
|
+
*/
|
|
33
|
+
@Component({
|
|
34
|
+
selector: 'ba-providers-section',
|
|
35
|
+
standalone: true,
|
|
36
|
+
// El elemento host es hijo directo del grid de content-signup, pero las clases
|
|
37
|
+
// de posicionamiento (bas-row-start-2, sm1:bas-row-span-2, etc.) van en el
|
|
38
|
+
// <section> interno. `display: contents` hace que ese <section> sea el grid
|
|
39
|
+
// item real (como en React, donde el div raíz ES el hijo del grid). Sin esto
|
|
40
|
+
// el host se auto-coloca y el layout del modal se solapa.
|
|
41
|
+
host: { style: 'display: contents' },
|
|
42
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
43
|
+
imports: [ProvidersFiltersComponent, ProvidersButtonsComponent],
|
|
44
|
+
template: `
|
|
45
|
+
@if (hasConfig()) {
|
|
46
|
+
<section
|
|
47
|
+
data-testid="providers-section"
|
|
48
|
+
[class]="'bas-box-border bas-grid bas-items-center bas-justify-center ' + sectionContainerClassNames()"
|
|
49
|
+
>
|
|
50
|
+
<div class="bas-box-border bas-flex bas-h-[52px] bas-w-full bas-flex-row bas-items-center bas-justify-start bas-gap-2 bas-pl-2">
|
|
51
|
+
<img src="https://dashboard.block-auth.io/img/blockauth_logo.png" width="34" height="34" alt="Block-Auth" />
|
|
52
|
+
<h2 [class]="'bas-m-0 bas-text-2xl bas-font-normal bas-text-[#00509a] ' + h2ClassNames()">
|
|
53
|
+
{{ titleTxt() ?? t('connectWith') }}
|
|
54
|
+
</h2>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<ba-providers-filters
|
|
58
|
+
[container]="container()"
|
|
59
|
+
[uiConfigProvidersInfo]="info()"
|
|
60
|
+
[searchProviderValue]="searchProviderValue()"
|
|
61
|
+
(changeProviderSearch)="onChangeProviderSearchValue($event)"
|
|
62
|
+
[openWebType]="openWebType()"
|
|
63
|
+
(openWebTypeChange)="openWebType.set($event)"
|
|
64
|
+
[selectedWebType]="selectedWebType()"
|
|
65
|
+
[webTypesItems]="webTypesItems()"
|
|
66
|
+
(changeWebType)="onChangeWebTypeItem($event)"
|
|
67
|
+
[isEditable]="isEditable()"
|
|
68
|
+
[isEditing]="isEditing()"
|
|
69
|
+
[device]="device()"
|
|
70
|
+
/>
|
|
71
|
+
<ba-providers-buttons
|
|
72
|
+
[providersFiltersEnabled]="providersFiltersEnabled()"
|
|
73
|
+
[isMobile]="isMobileLayout()"
|
|
74
|
+
[uiConfigProvidersInfo]="info()"
|
|
75
|
+
[showedProviders]="showedProviders()"
|
|
76
|
+
[selectedService]="selectedService()"
|
|
77
|
+
(selectedServiceChange)="selectedServiceChange.emit($event)"
|
|
78
|
+
[isEditable]="isEditable()"
|
|
79
|
+
[isEditing]="isEditing()"
|
|
80
|
+
[device]="device() === 'phone' ? 'phone' : 'desktop'"
|
|
81
|
+
/>
|
|
82
|
+
</section>
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
})
|
|
86
|
+
export class ProvidersSectionComponent {
|
|
87
|
+
container = input<HTMLElement | null>(null);
|
|
88
|
+
// Acepta `undefined` (ContentSignup lo pasa así cuando no hay config). En
|
|
89
|
+
// react un prop `undefined` recae en el default por parámetro; en Angular una
|
|
90
|
+
// binding `undefined` PISA el default de `input()`, así que se recae al
|
|
91
|
+
// default vía el `info()` computed.
|
|
92
|
+
uiConfigProvidersInfo = input<
|
|
93
|
+
| {
|
|
94
|
+
providersColumns?: number;
|
|
95
|
+
showProviderNameOnMobile?: boolean;
|
|
96
|
+
enableSearch?: boolean;
|
|
97
|
+
enableWebTypeFilter?: boolean;
|
|
98
|
+
}
|
|
99
|
+
| undefined
|
|
100
|
+
>(undefined);
|
|
101
|
+
|
|
102
|
+
private readonly DEFAULT_INFO = {
|
|
103
|
+
providersColumns: 3,
|
|
104
|
+
showProviderNameOnMobile: true,
|
|
105
|
+
enableSearch: true,
|
|
106
|
+
enableWebTypeFilter: true,
|
|
107
|
+
};
|
|
108
|
+
protected info = computed(() => this.uiConfigProvidersInfo() ?? this.DEFAULT_INFO);
|
|
109
|
+
selectedService = input('');
|
|
110
|
+
showedProviders = input<string[]>([]);
|
|
111
|
+
isEditable = input(false);
|
|
112
|
+
isEditing = input(false);
|
|
113
|
+
device = input<'desktop' | 'tablet' | 'phone'>('desktop');
|
|
114
|
+
titleTxt = input<string | undefined>(undefined);
|
|
115
|
+
|
|
116
|
+
selectedServiceChange = output<string>();
|
|
117
|
+
changeWebType = output<WebTypeItem>();
|
|
118
|
+
changeProviderSearch = output<string>();
|
|
119
|
+
|
|
120
|
+
private readonly locale = injectStore($locale);
|
|
121
|
+
protected t = (key: TextKey): string => translate(key, this.locale());
|
|
122
|
+
|
|
123
|
+
// Recalcula (vía `t`, que lee `$locale`) cuando cambia el idioma, igual que
|
|
124
|
+
// el `useMemo(..., [locale])` de react.
|
|
125
|
+
protected webTypesItems = computed(() => makeWebTypeItems(this.t));
|
|
126
|
+
protected searchProviderValue = signal('');
|
|
127
|
+
private readonly selectedWebTypeValue = signal('all');
|
|
128
|
+
protected selectedWebType = computed(
|
|
129
|
+
() => this.webTypesItems().find((item) => item.value === this.selectedWebTypeValue()) ?? this.webTypesItems()[0],
|
|
130
|
+
);
|
|
131
|
+
protected openWebType = signal(false);
|
|
132
|
+
|
|
133
|
+
// useMobile(660): SSR-safe + reactivo a resize.
|
|
134
|
+
private readonly platformId = inject(PLATFORM_ID);
|
|
135
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
136
|
+
private readonly isMobile = signal(false);
|
|
137
|
+
|
|
138
|
+
constructor() {
|
|
139
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
140
|
+
const onResize = () => this.isMobile.set(window.innerWidth < 660);
|
|
141
|
+
onResize();
|
|
142
|
+
window.addEventListener('resize', onResize);
|
|
143
|
+
this.destroyRef.onDestroy(() => window.removeEventListener('resize', onResize));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected hasConfig = computed(() => Object.keys(this.info()).length !== 0);
|
|
148
|
+
protected providersFiltersEnabled = computed(
|
|
149
|
+
() => !!this.info().enableSearch || !!this.info().enableWebTypeFilter,
|
|
150
|
+
);
|
|
151
|
+
protected isMobileLayout = computed(() => this.isMobile() || (this.isEditable() && this.device() === 'phone'));
|
|
152
|
+
|
|
153
|
+
onChangeWebTypeItem(webType: WebTypeItem): void {
|
|
154
|
+
this.selectedWebTypeValue.set(webType.value);
|
|
155
|
+
this.changeWebType.emit(webType);
|
|
156
|
+
}
|
|
157
|
+
onChangeProviderSearchValue(value: string): void {
|
|
158
|
+
this.searchProviderValue.set(value);
|
|
159
|
+
this.changeProviderSearch.emit(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ----------------- CLASSNAMES (port literal) -----------------
|
|
163
|
+
protected useTallRows = computed(() => this.isMobileLayout() || this.providersFiltersEnabled());
|
|
164
|
+
protected sectionContainerClassNames = computed(() => {
|
|
165
|
+
if (!this.isEditable()) {
|
|
166
|
+
return `bas-row-start-2 bas-h-[140px] bas-grid-cols-[auto_1fr] bas-grid-rows-[52px_76px]
|
|
167
|
+
bas-gap-1 bas-pt-2 xsm1:bas-w-full sm1:bas-row-span-2 sm1:bas-row-start-auto
|
|
168
|
+
sm1:bas-h-[282px] sm1:bas-w-60 sm1:bas-grid-cols-1 sm1:bas-gap-0 sm1:bas-pt-0
|
|
169
|
+
${this.useTallRows() ? 'sm1:bas-grid-rows-[52px_42px_188px]' : 'sm1:bas-grid-rows-[52px_0px_230px]'}`;
|
|
170
|
+
}
|
|
171
|
+
if (this.device() === 'phone') {
|
|
172
|
+
return 'bas-row-start-2 bas-h-[140px] bas-grid-cols-[auto_1fr] bas-grid-rows-[52px_76px] bas-gap-1 bas-pt-2';
|
|
173
|
+
}
|
|
174
|
+
return `bas-row-span-2 bas-row-start-auto bas-h-[282px] bas-w-60 bas-grid-cols-1
|
|
175
|
+
bas-gap-0 bas-pt-0 ${this.useTallRows() ? 'bas-grid-rows-[52px_42px_188px]' : 'bas-grid-rows-[52px_0px_230px]'}`;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
protected bothFilters = computed(
|
|
179
|
+
() => !!this.info().enableSearch && !!this.info().enableWebTypeFilter,
|
|
180
|
+
);
|
|
181
|
+
protected h2ClassNames = computed(() => {
|
|
182
|
+
if (!this.isMobileLayout()) return '';
|
|
183
|
+
if (this.isEditable()) return this.device() === 'phone' ? 'bas-hidden' : 'bas-block';
|
|
184
|
+
if (this.providersFiltersEnabled()) return `bas-hidden ${this.bothFilters() ? 'xsm1:bas-block' : 'xsm:bas-block'}`;
|
|
185
|
+
return '';
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/angular';
|
|
3
|
+
import { PermissionsRequiredModalComponent } from './permissions-required-modal.component';
|
|
4
|
+
|
|
5
|
+
// Port de packages/react/src/components/txns/internals/PermissionsRequiredModal.test.tsx
|
|
6
|
+
// (mapa: packages/vue/.../PermissionsRequiredModal.test.ts).
|
|
7
|
+
describe('PermissionsRequiredModal', () => {
|
|
8
|
+
it('renderiza el modal de permisos requeridos', async () => {
|
|
9
|
+
await render(PermissionsRequiredModalComponent);
|
|
10
|
+
expect(screen.getByText('Temporal access to identity')).toBeDefined();
|
|
11
|
+
expect(screen.getByText('Access to the list of authorised apps')).toBeDefined();
|
|
12
|
+
expect(screen.getByText('Only this transaction will be signed')).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
// Port literal de packages/react/src/components/txns/internals/PermissionsRequiredModal.tsx
|
|
4
|
+
// (mapa: packages/vue/.../PermissionsRequiredModal.vue). Sin props/outputs, sin
|
|
5
|
+
// lógica de core: solo itera un array fijo de permisos.
|
|
6
|
+
const iconClassNames = 'bas-w-6 bas-h-6 bas-text-[#009F6B]';
|
|
7
|
+
const requiredPermissions = [
|
|
8
|
+
{ message: 'Temporal access to identity', icon: '👤' },
|
|
9
|
+
{ message: 'Access to the list of authorised apps', icon: '📝' },
|
|
10
|
+
{ message: 'Only this transaction will be signed', icon: '🔄' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ba-permissions-required-modal',
|
|
15
|
+
standalone: true,
|
|
16
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
|
+
template: `
|
|
18
|
+
<div
|
|
19
|
+
class="bal-ba-transition-2 bas-box-border bas-grid bas-w-[334px] bas-grid-rows-3 bas-gap-3 bas-rounded-md bas-border-2 bas-border-solid bas-border-[var(--bal-primary-100)] bas-bg-[var(--bal-primary-10)] bas-py-4 bas-pl-3 hover:bas-border-[var(--bal-primary-300)]"
|
|
20
|
+
>
|
|
21
|
+
@for (permission of requiredPermissions; track $index) {
|
|
22
|
+
<div class="bas-flex bas-flex-row bas-items-center bas-gap-2">
|
|
23
|
+
<span [class]="iconClassNames" aria-hidden="true">{{ permission.icon }}</span>
|
|
24
|
+
<p class="bas-m-0 bas-text-start bas-text-base bas-text-[var(--bal-primary-800)]">
|
|
25
|
+
{{ permission.message }}
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
`,
|
|
31
|
+
})
|
|
32
|
+
export class PermissionsRequiredModalComponent {
|
|
33
|
+
protected readonly iconClassNames = iconClassNames;
|
|
34
|
+
protected readonly requiredPermissions = requiredPermissions;
|
|
35
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/angular';
|
|
4
|
+
import { SolicitedDataModalComponent, type ParamChange } from './solicited-data-modal.component';
|
|
5
|
+
|
|
6
|
+
// Port de packages/react/src/components/txns/internals/SolicitedDataModal.test.tsx
|
|
7
|
+
// (mapa: packages/vue/.../SolicitedDataModal.test.ts). Presentacional, sin client.
|
|
8
|
+
describe('SolicitedDataModal', () => {
|
|
9
|
+
it('renderiza el modal de datos solicitados con campos requeridos y opcionales', async () => {
|
|
10
|
+
@Component({
|
|
11
|
+
standalone: true,
|
|
12
|
+
imports: [SolicitedDataModalComponent],
|
|
13
|
+
template: `<ba-solicited-data-modal [requiredParams]="['users-email']" [optionalParams]="['users-phone']" />`,
|
|
14
|
+
})
|
|
15
|
+
class Host {}
|
|
16
|
+
await render(Host);
|
|
17
|
+
expect(screen.getByText('Email')).toBeDefined();
|
|
18
|
+
expect(screen.getByText('Phone')).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('emite paramChange al escribir y validationChange según validez', async () => {
|
|
22
|
+
const paramChanges: ParamChange[] = [];
|
|
23
|
+
const validationChanges: boolean[] = [];
|
|
24
|
+
@Component({
|
|
25
|
+
standalone: true,
|
|
26
|
+
imports: [SolicitedDataModalComponent],
|
|
27
|
+
template: `<ba-solicited-data-modal
|
|
28
|
+
[requiredParams]="['users-email']"
|
|
29
|
+
[optionalParams]="[]"
|
|
30
|
+
[userParamValues]="vals"
|
|
31
|
+
(paramChange)="pc($event)"
|
|
32
|
+
(validationChange)="vc($event)"
|
|
33
|
+
/>`,
|
|
34
|
+
})
|
|
35
|
+
class Host {
|
|
36
|
+
vals: Record<string, string> = {};
|
|
37
|
+
pc = (e: ParamChange) => paramChanges.push(e);
|
|
38
|
+
vc = (v: boolean) => validationChanges.push(v);
|
|
39
|
+
}
|
|
40
|
+
const { fixture } = await render(Host);
|
|
41
|
+
|
|
42
|
+
// Sin params requeridos rellenados -> invalid emitido de inmediato (effect).
|
|
43
|
+
expect(validationChanges[validationChanges.length - 1]).toBe(false);
|
|
44
|
+
|
|
45
|
+
const input = screen.getByRole('textbox');
|
|
46
|
+
await fireEvent.input(input, { target: { value: 'not-an-email' } });
|
|
47
|
+
expect(paramChanges[paramChanges.length - 1]).toEqual({ slug: 'users-email', value: 'not-an-email' });
|
|
48
|
+
|
|
49
|
+
fixture.componentInstance.vals = { 'users-email': 'a@b.com' };
|
|
50
|
+
fixture.detectChanges();
|
|
51
|
+
await fixture.whenStable();
|
|
52
|
+
fixture.detectChanges();
|
|
53
|
+
expect(validationChanges[validationChanges.length - 1]).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('muestra el error inline tras blur con un valor invalido', async () => {
|
|
57
|
+
@Component({
|
|
58
|
+
standalone: true,
|
|
59
|
+
imports: [SolicitedDataModalComponent],
|
|
60
|
+
template: `<ba-solicited-data-modal
|
|
61
|
+
[requiredParams]="['users-email']"
|
|
62
|
+
[optionalParams]="[]"
|
|
63
|
+
[userParamValues]="{ 'users-email': 'not-an-email' }"
|
|
64
|
+
/>`,
|
|
65
|
+
})
|
|
66
|
+
class Host {}
|
|
67
|
+
await render(Host);
|
|
68
|
+
const input = screen.getByRole('textbox');
|
|
69
|
+
await fireEvent.blur(input);
|
|
70
|
+
expect(screen.getByText('Enter a valid email (e.g. name@domain.com)')).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
});
|