Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ coverage/
.sass-cache/
.versions/
node_modules/
.worktrees/
$RECYCLE.BIN/

.DS_Store
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@betha-plataforma/estrutura-componentes",
"version": "1.8.0",
"version": "1.9.0",
"description": "Coleção de Web Components para compor a estrutura de uma aplicação front-end da Betha Sistemas.",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
28 changes: 26 additions & 2 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,9 +554,21 @@ export namespace Components {
}
interface BthUtilitarios {
/**
* Utilitarios
* Configuração de autorização. Obrigatória quando `buscarUtilitarios` está habilitada (a api-utilitarios filtra a visibilidade pela identidade do token + User-Access).
*/
"authorization"?: AuthorizationConfig;
/**
* Habilita a busca dos utilitários centrais (api-utilitarios) visíveis ao usuário, concatenando-os aos definidos pelo sistema. Desligada por padrão: sem ela o componente apenas renderiza `utilitarios` (comportamento legado).
*/
"buscarUtilitarios": boolean;
/**
* Utilitários definidos pelo próprio sistema. São sempre exibidos, independente da busca central.
*/
"utilitarios": Array<Utilitario>;
/**
* Override do host da api-utilitarios. Por padrão é resolvido por ambiente (test, prod) a partir do `variaveis.js` da aplicação (`___bth.envs.suite.utilitarios.v1.host`).
*/
"utilitariosHost"?: string;
}
}
declare global {
Expand Down Expand Up @@ -1286,14 +1298,26 @@ declare namespace LocalJSX {
"licencasApi"?: string;
}
interface BthUtilitarios {
/**
* Configuração de autorização. Obrigatória quando `buscarUtilitarios` está habilitada (a api-utilitarios filtra a visibilidade pela identidade do token + User-Access).
*/
"authorization"?: AuthorizationConfig;
/**
* Habilita a busca dos utilitários centrais (api-utilitarios) visíveis ao usuário, concatenando-os aos definidos pelo sistema. Desligada por padrão: sem ela o componente apenas renderiza `utilitarios` (comportamento legado).
*/
"buscarUtilitarios"?: boolean;
/**
* É emitido quando algum utilitário for selecionado
*/
"onOpcaoUtilitarioSelecionada"?: (event: CustomEvent<OpcaoUtilitarioSelecionadaEvent>) => void;
/**
* Utilitarios
* Utilitários definidos pelo próprio sistema. São sempre exibidos, independente da busca central.
*/
"utilitarios"?: Array<Utilitario>;
/**
* Override do host da api-utilitarios. Por padrão é resolvido por ambiente (test, prod) a partir do `variaveis.js` da aplicação (`___bth.envs.suite.utilitarios.v1.host`).
*/
"utilitariosHost"?: string;
}
interface IntrinsicElements {
"bth-ajuda": BthAjuda;
Expand Down
34 changes: 31 additions & 3 deletions src/components/utilitarios/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,42 @@ utilitarios.addEventListener('opcaoUtilitarioSelecionada', function navegar(even
});
```

## Buscando utilitários centrais (api-utilitarios)

Além dos utilitários definidos pelo próprio sistema (prop `utilitarios`), o componente pode
buscar os **utilitários centrais** cadastrados na `api-utilitarios`
visíveis para a identidade do usuário e **concatená-los** à lista do sistema (dedup por `rota`,
os do sistema têm precedência).

A busca é **opt-in** pela flag `buscar-utilitarios` (desligada por padrão — sem ela o
comportamento é idêntico ao legado). Quando ligada, exige `authorization`. O host é resolvido por
ambiente (test/prod) a partir do `variaveis.js` da aplicação
(`___bth.envs.suite.utilitarios.v1.host`); pode ser sobrescrito pelo prop `utilitarios-host`.

Se a chamada falhar, o componente faz **fail-open**: renderiza apenas os utilitários do sistema.

```js
var utilitarios = document.querySelector('bth-utilitarios');

utilitarios.utilitarios = [ /* utilitários hardcoded do sistema */ ];

// habilita a busca central; host vem do variaveis.js, salvo override
utilitarios.buscarUtilitarios = true;
utilitarios.authorization = authorizationConfig; // { getAuthorization(), handleUnauthorizedAccess() }
// utilitarios.utilitariosHost = 'https://utilitarios.test.betha.cloud'; // opcional (override)
```

<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| ------------- | --------- | ----------- | -------------- | ----------- |
| `utilitarios` | -- | Utilitarios | `Utilitario[]` | `undefined` |
| Property | Attribute | Description | Type | Default |
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | ----------- |
| `authorization` | -- | Configuração de autorização. Obrigatória quando `buscarUtilitarios` está habilitada (a api-utilitarios filtra a visibilidade pela identidade do token + User-Access). | `AuthorizationConfig` | `undefined` |
| `buscarUtilitarios` | `buscar-utilitarios` | Habilita a busca dos utilitários centrais (api-utilitarios) visíveis ao usuário, concatenando-os aos definidos pelo sistema. Desligada por padrão: sem ela o componente apenas renderiza `utilitarios` (comportamento legado). | `boolean` | `false` |
| `utilitarios` | -- | Utilitários definidos pelo próprio sistema. São sempre exibidos, independente da busca central. | `Utilitario[]` | `undefined` |
| `utilitariosHost` | `utilitarios-host` | Override do host da api-utilitarios. Por padrão é resolvido por ambiente (test, prod) a partir do `variaveis.js` da aplicação (`___bth.envs.suite.utilitarios.v1.host`). | `string` | `undefined` |


## Events
Expand Down
58 changes: 58 additions & 0 deletions src/components/utilitarios/test/utilitarios.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { setGlobalOrWindowProperty } from '../../../../test/utils/spec.helper';
import { AuthorizationConfig } from '../../../global/interfaces';
import { UtilitariosService } from '../utilitarios.service';

describe('UtilitariosService', () => {
let service: UtilitariosService;
let authorization: AuthorizationConfig;

beforeEach(() => {
authorization = {
getAuthorization: () => ({ accessToken: 'ACCESS_TOKEN' }),
handleUnauthorizedAccess: () => Promise.resolve()
};

service = new UtilitariosService(authorization, 'https://utilitarios.test');
});

it('busca a lista crua no endpoint /api/utilitarios do host informado', async () => {
const fetchMock = jest.fn().mockImplementation(() => Promise.resolve({
status: 200,
json: () => Promise.resolve([
{ id: '1', nome: 'Auditoria', rota: '/auditoria', icone: 'shield', possuiPermissao: true }
])
}));
setGlobalOrWindowProperty(global, 'fetch', fetchMock);

const utilitarios = await service.listar();

expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0][0]).toBe('https://utilitarios.test/api/utilitarios');
expect(utilitarios).toEqual([
{ nome: 'Auditoria', rota: '/auditoria', icone: 'shield', possuiPermissao: true }
]);
});

it('assume possuiPermissao true quando ausente na resposta', async () => {
const fetchMock = jest.fn().mockImplementation(() => Promise.resolve({
status: 200,
json: () => Promise.resolve([{ nome: 'X', rota: '/x', icone: 'star' }])
}));
setGlobalOrWindowProperty(global, 'fetch', fetchMock);

const [utilitario] = await service.listar();

expect(utilitario.possuiPermissao).toBe(true);
});

it('propaga erro quando a api-utilitarios responde fora do range 2xx', async () => {
const fetchMock = jest.fn().mockImplementation(() => Promise.resolve({
status: 500,
statusText: 'Internal Server Error'
}));
setGlobalOrWindowProperty(global, 'fetch', fetchMock);

await expect(service.listar()).rejects.toBeDefined();
});

});
111 changes: 111 additions & 0 deletions src/components/utilitarios/test/utilitarios.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { newSpecPage, SpecPage } from '@stencil/core/testing';

import { setBethaEnvs, setGlobalOrWindowProperty } from '../../../../test/utils/spec.helper';
import { AuthorizationConfig } from '../../../global/interfaces';
import { Utilitarios } from '../utilitarios';

const flushPromises = () => new Promise<void>(resolve => setTimeout(resolve, 0));

const autorizacaoValida = (): AuthorizationConfig => ({
getAuthorization: () => ({ accessToken: 'ACCESS_TOKEN' }),
handleUnauthorizedAccess: () => Promise.resolve()
});

const nomesDoPainel = (utilitarios: HTMLBthUtilitariosElement): Array<string> =>
Array.from(utilitarios.shadowRoot.querySelectorAll('[slot=conteudo_painel_lateral] .descricao'))
.map(elemento => elemento.textContent);

describe('utilitarios', () => {
let page: SpecPage;

beforeEach(async () => {
page = await newSpecPage({ components: [Utilitarios] });
});

afterEach(() => {
delete (window as any)['___bth'];
});

it('renderiza icone desktop', async () => {
// Act
await page.setContent('<bth-utilitarios></bth-utilitarios>');
Expand Down Expand Up @@ -142,4 +159,98 @@ describe('utilitarios', () => {
expect(onOpcaoUtilitarioSelecionada).not.toHaveBeenCalled();
});

it('não busca utilitários centrais com a flag desligada (comportamento legado)', async () => {
// Arrange
const fetchMock = jest.fn();
setGlobalOrWindowProperty(global, 'fetch', fetchMock);

await page.setContent('<bth-utilitarios></bth-utilitarios>');
const utilitarios: HTMLBthUtilitariosElement = page.doc.querySelector('bth-utilitarios');

// Act — mesmo com authorization e host, sem a flag não há chamada
utilitarios.utilitarios = [{ nome: 'Do Sistema', rota: '/sistema', icone: 'key', possuiPermissao: true }];
utilitarios.utilitariosHost = 'https://utilitarios.test';
utilitarios.authorization = autorizacaoValida();
await flushPromises();
await page.waitForChanges();

// Assert
expect(fetchMock).not.toHaveBeenCalled();
expect(nomesDoPainel(utilitarios)).toEqual(['Do Sistema']);
});

it('concatena os utilitários centrais aos do sistema, deduplicando por rota', async () => {
// Arrange
const fetchMock = jest.fn().mockImplementation(() => Promise.resolve({
status: 200,
json: () => Promise.resolve([
{ id: '1', nome: 'Auditoria', rota: '/auditoria', icone: 'shield', possuiPermissao: true },
{ id: '2', nome: 'Global Central', rota: '/global', icone: 'earth', possuiPermissao: true }
])
}));
setGlobalOrWindowProperty(global, 'fetch', fetchMock);

await page.setContent('<bth-utilitarios></bth-utilitarios>');
const utilitarios: HTMLBthUtilitariosElement = page.doc.querySelector('bth-utilitarios');

// Act
utilitarios.utilitarios = [{ nome: 'Do Sistema', rota: '/global', icone: 'key', possuiPermissao: true }];
utilitarios.utilitariosHost = 'https://utilitarios.test';
utilitarios.authorization = autorizacaoValida();
utilitarios.buscarUtilitarios = true;
await flushPromises();
await page.waitForChanges();

// Assert — sistema primeiro, central não-duplicado depois; '/global' duplicado é descartado
expect(fetchMock.mock.calls[0][0]).toBe('https://utilitarios.test/api/utilitarios');
expect(nomesDoPainel(utilitarios)).toEqual(['Do Sistema', 'Auditoria']);
});

it('resolve o host por ambiente a partir do variaveis.js (___bth) quando o prop não é informado', async () => {
// Arrange
setBethaEnvs({ suite: { utilitarios: { v1: { host: 'https://utilitarios.env' } } } });
const fetchMock = jest.fn().mockImplementation(() => Promise.resolve({
status: 200,
json: () => Promise.resolve([{ nome: 'Central', rota: '/central', icone: 'star', possuiPermissao: true }])
}));
setGlobalOrWindowProperty(global, 'fetch', fetchMock);

await page.setContent('<bth-utilitarios></bth-utilitarios>');
const utilitarios: HTMLBthUtilitariosElement = page.doc.querySelector('bth-utilitarios');

// Act
utilitarios.utilitarios = [];
utilitarios.authorization = autorizacaoValida();
utilitarios.buscarUtilitarios = true;
await flushPromises();
await page.waitForChanges();

// Assert
expect(fetchMock.mock.calls[0][0]).toBe('https://utilitarios.env/api/utilitarios');
expect(nomesDoPainel(utilitarios)).toEqual(['Central']);
});

it('faz fail-open exibindo apenas os do sistema quando a busca central falha', async () => {
// Arrange
const fetchMock = jest.fn().mockImplementation(() => Promise.resolve({ status: 500, statusText: 'erro' }));
setGlobalOrWindowProperty(global, 'fetch', fetchMock);
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);

await page.setContent('<bth-utilitarios></bth-utilitarios>');
const utilitarios: HTMLBthUtilitariosElement = page.doc.querySelector('bth-utilitarios');

// Act
utilitarios.utilitarios = [{ nome: 'Do Sistema', rota: '/sistema', icone: 'key', possuiPermissao: true }];
utilitarios.utilitariosHost = 'https://utilitarios.test';
utilitarios.authorization = autorizacaoValida();
utilitarios.buscarUtilitarios = true;
await flushPromises();
await page.waitForChanges();

// Assert
expect(nomesDoPainel(utilitarios)).toEqual(['Do Sistema']);
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});

});
29 changes: 29 additions & 0 deletions src/components/utilitarios/utilitarios.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Api } from '../../global/api';
import { AuthorizationConfig } from '../../global/interfaces';
import { Utilitario } from './utilitarios.interfaces';

export class UtilitariosService {

private api: Api;

constructor(authorization: AuthorizationConfig, utilitariosHost: string) {
this.api = new Api(authorization.getAuthorization(), authorization.handleUnauthorizedAccess, utilitariosHost);
}

/**
* Lista os utilitários centrais visíveis ao usuário (filtrados pela própria
* identidade do token + User-Access no servidor). A api-utilitarios devolve
* um array cru já ordenado por nome.
*/
async listar(): Promise<Array<Utilitario>> {
return this.api.request('GET', 'api/utilitarios')
.then(response => response.json())
.then((itens: Array<Utilitario>) => itens.map(item => ({
nome: item.nome,
icone: item.icone,
rota: item.rota,
possuiPermissao: item.possuiPermissao !== false
})));
}

}
Loading
Loading