automacao-core-carga-back 1.0.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.
@@ -0,0 +1,766 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var fs = require('node:fs');
6
+ var path = require('node:path');
7
+ var test$1 = require('@playwright/test');
8
+ var automacaoCorePlaywright = require('automacao-core-playwright');
9
+ var sharp = require('sharp');
10
+
11
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
12
+
13
+ var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
14
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
15
+ var sharp__default = /*#__PURE__*/_interopDefaultLegacy(sharp);
16
+
17
+ /**
18
+ * `true` se o arquivo existe e não está vazio.
19
+ * @param {string} caminho - Caminho completo do arquivo
20
+ * @returns {boolean}
21
+ */
22
+ function arquivoNaoVazio(caminho) {
23
+ try {
24
+ return fs__default["default"].existsSync(caminho) && fs__default["default"].statSync(caminho).size > 0;
25
+ } catch (_) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Page Object com interações genéricas compartilhadas entre todos os specs
32
+ * de teste de carga: login APM/Grafana, capturas de tela e combinação de imagens.
33
+ */
34
+ class ComunsUtils {
35
+ /**
36
+ * @param {import('@playwright/test').Page} page
37
+ */
38
+ constructor(page) {
39
+ this.page = page;
40
+ this.dataUtils = new automacaoCorePlaywright.DataUtils(page);
41
+ this.formUtils = new automacaoCorePlaywright.FormUtils(page);
42
+ this.navigationUtils = new automacaoCorePlaywright.NavigationUtils(page);
43
+ const now = new Date();
44
+ this.ano = now.getFullYear();
45
+ this.mes = String(now.getMonth() + 1).padStart(2, '0');
46
+ this.dia = String(now.getDate()).padStart(2, '0');
47
+
48
+ // Locators — Grafana
49
+ this.locatorBemVindoGrafanaHeader = page.getByRole('heading', {
50
+ name: 'Welcome to Grafana'
51
+ });
52
+ this.locatorUsernameInput = page.getByTestId('data-testid Username input field');
53
+ this.locatorSenhaInput = page.getByTestId('data-testid Password input field');
54
+ this.locatorLoginButton = page.getByTestId('data-testid Login button');
55
+
56
+ // Locators — APM (Elastic)
57
+ this.locatorLoadingElasticText = page.getByText('Loading Elastic');
58
+ this.locatorBemVindoElasticHeader = page.getByRole('heading', {
59
+ name: 'Welcome to Elastic'
60
+ });
61
+ this.locatorUsuarioInput = page.getByRole('textbox', {
62
+ name: 'Username'
63
+ });
64
+ this.locatorPasswordInput = page.getByRole('textbox', {
65
+ name: 'Password'
66
+ });
67
+ this.locatorLoginButtonApm = page.getByRole('button', {
68
+ name: 'Log in'
69
+ });
70
+ this.locatorCarregandoElasticText = page.getByText('Loading Elastic');
71
+ this.locatorElasticHomePage = page.getByRole('link', {
72
+ name: 'Elastic home'
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Aguarda N milissegundos — sempre o tempo cheio.
78
+ *
79
+ * Prefira `aguardarAte` quando houver uma condição observável: `delay` gasta o
80
+ * tempo todo mesmo quando a tela já está pronta.
81
+ *
82
+ * @param {number} ms
83
+ * @returns {Promise<void>}
84
+ */
85
+ async delay(ms) {
86
+ return new Promise(resolve => setTimeout(resolve, ms));
87
+ }
88
+
89
+ /**
90
+ * Aguarda até `condicao` retornar verdadeiro, verificando a cada `intervalo`.
91
+ * Retorna assim que a condição é satisfeita, sem gastar o tempo restante.
92
+ *
93
+ * Não lança no timeout: devolve `false` e deixa a decisão para quem chamou.
94
+ * Isso preserva a semântica de "melhor esforço" das esperas de acomodação que
95
+ * este método substituiu, sem o custo fixo quando a tela já está pronta.
96
+ *
97
+ * @param {() => (boolean | Promise<boolean>)} condicao - Verificação; exceção conta como não satisfeita
98
+ * @param {Object} [opcoes]
99
+ * @param {number} [opcoes.intervalo=100] - Intervalo entre verificações em ms
100
+ * @param {number} [opcoes.timeout=10000] - Tempo máximo de espera em ms
101
+ * @returns {Promise<boolean>} `true` se satisfeita; `false` se estourou o timeout
102
+ * @example
103
+ * const pronto = await page.comunsUtils.aguardarAte(
104
+ * () => page.locator('#painel').isVisible(),
105
+ * { timeout: 2000 },
106
+ * );
107
+ */
108
+ async aguardarAte(condicao, opcoes = {}) {
109
+ const {
110
+ intervalo = 100,
111
+ timeout = 10000
112
+ } = opcoes;
113
+ const limite = Date.now() + timeout;
114
+ for (;;) {
115
+ try {
116
+ if (await condicao()) return true;
117
+ } catch (_) {/* elemento ausente ou página navegando: conta como não satisfeita */}
118
+ const restante = limite - Date.now();
119
+ if (restante <= 0) return false;
120
+ await this.delay(Math.min(intervalo, restante));
121
+ }
122
+ }
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // Geração de URL com data dinâmica
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Atualiza os parâmetros de intervalo de tempo de uma URL com horários locais
130
+ * convertidos para ISO UTC (offset +3h para compensar UTC-3 Brasil).
131
+ * @param {string} baseUrl - URL base a ser atualizada
132
+ * @param {string} horarioInicial - Horário de início no formato "HH:mm:ss"
133
+ * @param {string} horarioFinal - Horário de fim no formato "HH:mm:ss"
134
+ * @param {string} paramInicio - Nome do query param de início (ex: 'rangeFrom' ou 'from')
135
+ * @param {string} paramFim - Nome do query param de fim (ex: 'rangeTo' ou 'to')
136
+ * @param {string|null} [data=null] - Data no formato "DD-MM-YYYY"; se null usa a data atual
137
+ * @returns {string} URL com os parâmetros de intervalo atualizados
138
+ * @private
139
+ */
140
+ _gerarUrlComDataAtual(baseUrl, horarioInicial, horarioFinal, paramInicio, paramFim, data = null) {
141
+ let ano, dia, mes;
142
+ if (data) {
143
+ const partes = data.split('-');
144
+ dia = partes[0];
145
+ mes = partes[1];
146
+ ano = partes[2];
147
+ } else {
148
+ ano = this.ano;
149
+ mes = this.mes;
150
+ dia = this.dia;
151
+ }
152
+ const parseHorario = horario => {
153
+ const partes = horario.split(':');
154
+ const hora = String(parseInt(partes[0]) + 3).padStart(2, '0'); // UTC-3 → UTC
155
+ const min = partes[1].padStart(2, '0');
156
+ const seg = (partes[2] ? partes[2].split('.')[0] : '00').padStart(2, '0');
157
+ return `${ano}-${mes}-${dia}T${hora}:${min}:${seg}.000Z`;
158
+ };
159
+ const urlAtualizada = baseUrl.replace(new RegExp(`[&?]${paramInicio}=[^&]*`), '').replace(new RegExp(`[&?]${paramFim}=[^&]*`), '');
160
+ const sep = urlAtualizada.includes('?') ? '&' : '?';
161
+ return `${urlAtualizada}${sep}${paramInicio}=${encodeURIComponent(parseHorario(horarioInicial))}&${paramFim}=${encodeURIComponent(parseHorario(horarioFinal))}`;
162
+ }
163
+
164
+ /**
165
+ * Gera URL do APM com intervalo de tempo atualizado para o dia atual.
166
+ * Atualiza os parâmetros `rangeFrom` e `rangeTo`.
167
+ * @param {string} baseUrl - URL base do APM
168
+ * @param {string} horarioInicial - Ex: "09:40:00"
169
+ * @param {string} horarioFinal - Ex: "10:50:00"
170
+ * @param {string|null} [data=null] - Data no formato "DD-MM-YYYY" (opcional)
171
+ * @returns {string} URL atualizada
172
+ */
173
+ gerarUrlApmComDataAtual(baseUrl, horarioInicial, horarioFinal, data = null) {
174
+ return this._gerarUrlComDataAtual(baseUrl, horarioInicial, horarioFinal, 'rangeFrom', 'rangeTo', data);
175
+ }
176
+
177
+ /**
178
+ * Gera URL do Grafana com intervalo de tempo atualizado para o dia atual.
179
+ * Atualiza os parâmetros `from` e `to`.
180
+ * @param {string} baseUrl - URL base do Grafana
181
+ * @param {string} horarioInicial - Ex: "10:37:00"
182
+ * @param {string} horarioFinal - Ex: "11:11:00"
183
+ * @param {string|null} [data=null] - Data no formato "DD-MM-YYYY" (opcional)
184
+ * @returns {string} URL atualizada
185
+ */
186
+ gerarUrlGrafanaComDataAtual(baseUrl, horarioInicial, horarioFinal, data = null) {
187
+ return this._gerarUrlComDataAtual(baseUrl, horarioInicial, horarioFinal, 'from', 'to', data);
188
+ }
189
+
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ // Login
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+
194
+ /**
195
+ * Realiza login no Grafana.
196
+ * @param {Object} usuario - { email: string, senha: string }
197
+ */
198
+ async loginGrafana(usuario) {
199
+ await this.page.goto('https://grafana.senior.com.br/login');
200
+ await test$1.expect(this.locatorBemVindoGrafanaHeader).toBeVisible();
201
+ await this.locatorUsernameInput.type(usuario.email);
202
+ await this.locatorSenhaInput.type(usuario.senha);
203
+ await test$1.expect(this.locatorUsernameInput).toHaveValue(usuario.email);
204
+ await test$1.expect(this.locatorSenhaInput).toHaveValue(usuario.senha);
205
+ await this.locatorBemVindoGrafanaHeader.click();
206
+ await test$1.expect(this.locatorLoginButton).toBeVisible();
207
+ await this.locatorLoginButton.click();
208
+ await this.page.getByText('Loading').waitFor({
209
+ state: 'visible'
210
+ });
211
+ console.log(`Usuário ${usuario.email} logado no Grafana`);
212
+ }
213
+
214
+ /**
215
+ * Realiza login no APM Elastic.
216
+ * @param {Object} usuario - { usuario: string, senha: string }
217
+ */
218
+ async loginApm(usuario) {
219
+ await this.page.goto('https://apm.platform-homologx.senior.com.br:8443/login');
220
+ await test$1.expect(this.locatorLoadingElasticText).toBeVisible();
221
+ await test$1.expect(this.locatorBemVindoElasticHeader).toBeVisible();
222
+ await this.locatorUsuarioInput.type(usuario.usuario);
223
+ await this.locatorPasswordInput.type(usuario.senha);
224
+ await test$1.expect(this.locatorUsuarioInput).toHaveValue(usuario.usuario);
225
+ await test$1.expect(this.locatorPasswordInput).toHaveValue(usuario.senha);
226
+ await test$1.expect(this.locatorLoginButtonApm).toBeVisible();
227
+ await this.locatorLoginButtonApm.click();
228
+ await test$1.expect(this.locatorBemVindoElasticHeader).toBeHidden();
229
+ await test$1.expect(this.locatorCarregandoElasticText).toBeVisible();
230
+ await test$1.expect(this.locatorCarregandoElasticText).toBeHidden();
231
+ await test$1.expect(this.locatorElasticHomePage).toBeVisible();
232
+ console.log(`Usuário ${usuario.usuario} logado no APM`);
233
+ }
234
+
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+ // Screenshots
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+
239
+ /**
240
+ * Captura screenshot da página inteira com retry automático (3 tentativas).
241
+ * Cria o diretório de destino se não existir.
242
+ * @param {string} nomeArquivo - Nome do arquivo sem extensão
243
+ * @param {string} [caminhoCustomizado='playwright/resources/imagens'] - Diretório de destino
244
+ */
245
+ async tirarPrintTelaInteira(nomeArquivo, caminhoCustomizado = 'playwright/resources/imagens') {
246
+ try {
247
+ await this.page.waitForLoadState('networkidle', {
248
+ timeout: 15000
249
+ });
250
+ } catch (_) {/* ignora timeout — a página pode ter requests de longa duração */}
251
+ await this.page.evaluate(() => window.scrollTo(0, 0));
252
+ await this.aguardarAte(() => this.page.evaluate(() => window.scrollY === 0), {
253
+ timeout: 2000
254
+ });
255
+ const caminhoCompleto = `${caminhoCustomizado}/${nomeArquivo}.png`;
256
+ const diretorio = path__default["default"].dirname(caminhoCompleto);
257
+ if (!fs__default["default"].existsSync(diretorio)) {
258
+ fs__default["default"].mkdirSync(diretorio, {
259
+ recursive: true
260
+ });
261
+ console.log(`Diretório criado: ${diretorio}`);
262
+ }
263
+ const maxTentativas = 3;
264
+ let screenshotCriada = false;
265
+ for (let tentativa = 1; tentativa <= maxTentativas && !screenshotCriada; tentativa++) {
266
+ try {
267
+ await this.page.screenshot({
268
+ path: caminhoCompleto,
269
+ fullPage: true,
270
+ animations: 'disabled'
271
+ });
272
+ } catch (error) {
273
+ if (error.message.includes('Cannot take screenshot larger than 32767')) {
274
+ const vp = this.page.viewportSize();
275
+ await this.page.screenshot({
276
+ path: caminhoCompleto,
277
+ clip: {
278
+ x: 0,
279
+ y: 0,
280
+ width: vp.width,
281
+ height: 32760
282
+ },
283
+ animations: 'disabled'
284
+ });
285
+ } else if (['Unable to capture screenshot', 'Protocol error', 'Target closed', 'Session closed'].some(m => error.message.includes(m))) {
286
+ console.log(`⚠ Erro transitório (tentativa ${tentativa}/${maxTentativas}): ${error.message}`);
287
+ await this.aguardarAte(() => this.page.evaluate(() => true), {
288
+ timeout: 2000
289
+ });
290
+ try {
291
+ await this.page.waitForLoadState('networkidle', {
292
+ timeout: 10000
293
+ });
294
+ } catch (_) {/* ignora */}
295
+ } else {
296
+ throw error;
297
+ }
298
+ }
299
+ await this.aguardarAte(() => arquivoNaoVazio(caminhoCompleto), {
300
+ intervalo: 50,
301
+ timeout: 500
302
+ });
303
+ if (arquivoNaoVazio(caminhoCompleto)) {
304
+ screenshotCriada = true;
305
+ console.log(`✓ Screenshot salva (${fs__default["default"].statSync(caminhoCompleto).size} bytes): ${caminhoCompleto}`);
306
+ } else {
307
+ console.log(`⚠ Screenshot não gerada, tentando novamente (${tentativa}/${maxTentativas})...`);
308
+ }
309
+ }
310
+ if (!screenshotCriada) {
311
+ throw new Error(`Falha ao criar screenshot após ${maxTentativas} tentativas: ${caminhoCompleto}`);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Captura screenshot de um elemento específico via locator.
317
+ * @param {import('@playwright/test').Locator} locator - Locator do elemento
318
+ * @param {string} nomeArquivo - Nome do arquivo sem extensão
319
+ * @param {string} [caminhoCustomizado='playwright/resources/imagens'] - Diretório de destino
320
+ */
321
+ async tirarPrintLocator(locator, nomeArquivo, caminhoCustomizado = 'playwright/resources/imagens') {
322
+ await locator.scrollIntoViewIfNeeded();
323
+ await test$1.expect(locator).toBeVisible();
324
+ const caminhoCompleto = `${caminhoCustomizado}/${nomeArquivo}.png`;
325
+ await locator.screenshot({
326
+ path: caminhoCompleto,
327
+ animations: 'disabled'
328
+ });
329
+ console.log(`Screenshot do elemento salva: ${caminhoCompleto}`);
330
+ }
331
+
332
+ /**
333
+ * Combina múltiplas screenshots em uma única imagem vertical usando sharp.
334
+ * @param {string[]} nomesImagens - Nomes dos arquivos originais (sem extensão)
335
+ * @param {string} nomeArquivoFinal - Nome do arquivo combinado (sem extensão)
336
+ * @param {string} [caminhoOriginais='playwright/resources/imagens'] - Diretório das imagens originais
337
+ * @param {boolean} [deletarOriginais=false] - Se true, remove os arquivos originais após combinar
338
+ * @param {string|null} [caminhoDestino=null] - Diretório destino; se null usa caminhoOriginais
339
+ * @returns {Promise<string[]>} Caminhos das imagens individuais
340
+ * @example
341
+ * await page.comunsUtils.combinarPrints(
342
+ * ['bridge', 'prepareInvoice'],
343
+ * 'bridge-completo',
344
+ * 'playwright/resources/imagens/faturamento',
345
+ * true,
346
+ * pathReports,
347
+ * );
348
+ */
349
+ async combinarPrints(nomesImagens, nomeArquivoFinal, caminhoOriginais = 'playwright/resources/imagens', deletarOriginais = false, caminhoDestino = null) {
350
+ const caminhoSalvar = caminhoDestino || caminhoOriginais;
351
+ if (!fs__default["default"].existsSync(caminhoSalvar)) {
352
+ fs__default["default"].mkdirSync(caminhoSalvar, {
353
+ recursive: true
354
+ });
355
+ }
356
+ const caminhosImagens = nomesImagens.map(n => path__default["default"].join(caminhoOriginais, `${n}.png`));
357
+ const caminhoFinal = path__default["default"].join(caminhoSalvar, `${nomeArquivoFinal}.png`);
358
+ for (const c of caminhosImagens) {
359
+ if (!fs__default["default"].existsSync(c)) throw new Error(`Imagem não encontrada: ${c}`);
360
+ }
361
+ const metadados = await Promise.all(caminhosImagens.map(c => sharp__default["default"](c).metadata()));
362
+ const larguraMaxima = Math.max(...metadados.map(m => m.width));
363
+ const alturaTotal = metadados.reduce((s, m) => s + m.height, 0);
364
+ let posY = 0;
365
+ const composicoes = caminhosImagens.map((input, i) => {
366
+ const comp = {
367
+ input,
368
+ top: posY,
369
+ left: 0
370
+ };
371
+ posY += metadados[i].height;
372
+ return comp;
373
+ });
374
+ await sharp__default["default"]({
375
+ create: {
376
+ width: larguraMaxima,
377
+ height: alturaTotal,
378
+ channels: 4,
379
+ background: {
380
+ r: 255,
381
+ g: 255,
382
+ b: 255,
383
+ alpha: 1
384
+ }
385
+ }
386
+ }).composite(composicoes).toFile(caminhoFinal);
387
+ console.log(`Imagem combinada salva: ${caminhoFinal}`);
388
+ if (deletarOriginais) {
389
+ for (const c of caminhosImagens) {
390
+ fs__default["default"].unlinkSync(c);
391
+ console.log(`Original deletado: ${c}`);
392
+ }
393
+ }
394
+ return caminhosImagens;
395
+ }
396
+ }
397
+
398
+ const CSS_EUIACCORDIONBUTTON = 'button.euiAccordion__button';
399
+ const CSS_EUIICON_SVG = 'svg.euiIcon';
400
+ const CSS_PATHSETATRACE = 'path[d*="M1.957 4.982"]';
401
+ const CSS_EUIFLEXGROUP = '.euiFlexGroup.css-z23vzb-euiFlexGroup-xs-flexStart-stretch-column';
402
+ const REGEX_LOADING = /^Loading…$/;
403
+
404
+ /**
405
+ * Page Object para interações com o APM Elastic (Application Performance Monitoring).
406
+ * Encapsula navegação entre serviços, transactions e traces.
407
+ */
408
+ class ApmUtils {
409
+ /**
410
+ * @param {import('@playwright/test').Page} page
411
+ */
412
+ constructor(page) {
413
+ this.page = page;
414
+ this.comunsUtils = new ComunsUtils(page);
415
+ this.locatorElasticHome = page.getByRole('link', {
416
+ name: 'Elastic home'
417
+ });
418
+ this.locatorServicesHeader = page.getByText('Loaded Services');
419
+ this.locatorCarregamentoServices = page.locator('div').filter({
420
+ hasText: REGEX_LOADING
421
+ });
422
+ this.locatorCarregamentoInterno = page.getByRole('progressbar', {
423
+ name: 'Loading'
424
+ }).nth(2);
425
+ this.locatorTabelaText = page.locator(CSS_EUIFLEXGROUP);
426
+ this.locatorDismiss = page.getByRole('button', {
427
+ name: 'Dismiss',
428
+ exact: true
429
+ });
430
+ }
431
+
432
+ /**
433
+ * Navega para a URL do APM e aguarda o carregamento completo da lista de serviços.
434
+ * Clica no botão "Dismiss" se estiver visível.
435
+ * @param {string} url - URL do APM
436
+ */
437
+ async acessaTelaApm(url) {
438
+ await this.page.goto(url);
439
+ await this.page.waitForLoadState('networkidle');
440
+ await test$1.expect(this.locatorElasticHome).toBeVisible();
441
+ await test$1.expect(this.locatorServicesHeader).toBeVisible();
442
+ await test$1.expect(this.locatorCarregamentoServices).toBeHidden({
443
+ timeout: 180000
444
+ });
445
+ try {
446
+ await test$1.expect(this.locatorDismiss).toBeVisible({
447
+ timeout: 3000
448
+ });
449
+ await this.locatorDismiss.click();
450
+ console.log('Botão Dismiss clicado');
451
+ } catch (_) {
452
+ console.log('Botão Dismiss não encontrado, prosseguindo...');
453
+ }
454
+ console.log('Tela APM carregada com sucesso');
455
+ }
456
+
457
+ /**
458
+ * Acessa os detalhes de um serviço na lista do APM.
459
+ * Aguarda `detailed_statistics` e `dependencies` antes de prosseguir.
460
+ * Faz até 3 tentativas com reload em caso de falha.
461
+ * @param {string} nomeServico - Ex: 'bridge', 'erpx-fin-contas-pagar'
462
+ */
463
+ async acessarDetalhesServico(nomeServico) {
464
+ const servicoLink = this.page.getByRole('link', {
465
+ name: `opentelemetry/java ${nomeServico}`,
466
+ exact: true
467
+ });
468
+ const headerServico = this.page.locator('main').getByText(nomeServico, {
469
+ exact: true
470
+ }).first();
471
+ const maxTentativas = 3;
472
+ for (let t = 1; t <= maxTentativas; t++) {
473
+ try {
474
+ await test$1.expect(servicoLink).toBeVisible();
475
+ const statsPromise = this.page.waitForResponse(r => r.url().includes('/detailed_statistics') && r.status() === 200, {
476
+ timeout: 60000
477
+ });
478
+ const depsPromise = this.page.waitForResponse(r => r.url().includes('/dependencies') && r.status() === 200, {
479
+ timeout: 60000
480
+ });
481
+ await servicoLink.click();
482
+ await this.page.waitForLoadState('networkidle');
483
+ await test$1.expect(this.locatorCarregamentoInterno).toBeVisible();
484
+ await test$1.expect(this.locatorCarregamentoInterno).toBeHidden();
485
+ await Promise.all([statsPromise, depsPromise]);
486
+ await test$1.expect(headerServico).toBeVisible({
487
+ timeout: 30000
488
+ });
489
+ console.log(`Serviço acessado: ${nomeServico}`);
490
+ return;
491
+ } catch (error) {
492
+ console.log(`Tentativa ${t}/${maxTentativas} para "${nomeServico}" falhou.`);
493
+ if (t === maxTentativas) {
494
+ throw new Error(`Não foi possível acessar "${nomeServico}" após ${maxTentativas} tentativas. ${error.message}`);
495
+ }
496
+ await this.page.reload();
497
+ await this.page.waitForLoadState('networkidle');
498
+ }
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Acessa os detalhes de uma transaction específica dentro de um serviço.
504
+ * Aguarda `transactions`, `traces/` e `samples?` antes de prosseguir.
505
+ * @param {string} nomeTransaction - Ex: 'POST /erpx_fin/contas_receber/queries/gerarBaixas', 'calculaImpostos'
506
+ * @param {string} [traceExpandido='Não'] - 'Não' expande o trace; 'Sim' apenas valida o título
507
+ */
508
+ async acessarDetalhesTransactions(nomeTransaction, traceExpandido = 'Não') {
509
+ this.locatorHeaderTransaction = this.page.locator('h2').filter({
510
+ hasText: nomeTransaction
511
+ });
512
+ this.locatorTransactionLink = this.page.getByRole('link', {
513
+ name: nomeTransaction,
514
+ exact: true
515
+ });
516
+ this.locatorAccordionButton = this.page.locator(CSS_EUIACCORDIONBUTTON).filter({
517
+ has: this.page.locator('h5', {
518
+ hasText: nomeTransaction,
519
+ exact: true
520
+ })
521
+ });
522
+ this.locatorExpandirTrace = this.locatorAccordionButton.locator(CSS_EUIICON_SVG).locator('..').filter({
523
+ has: this.page.locator(CSS_PATHSETATRACE)
524
+ }).locator('svg').first();
525
+ await test$1.expect(this.locatorTransactionLink).toBeVisible();
526
+ await this.locatorTransactionLink.click();
527
+ const transactionsPromise = this.page.waitForResponse(r => r.url().includes('/transactions') && r.status() === 200);
528
+ const tracesPromise = this.page.waitForResponse(r => r.url().includes('/traces/') && r.status() === 200);
529
+ const samplePromise = this.page.waitForResponse(r => r.url().includes('/samples?') && r.status() === 200);
530
+ await this.page.waitForLoadState('networkidle');
531
+ await Promise.all([transactionsPromise, samplePromise, tracesPromise]);
532
+ await test$1.expect(this.locatorHeaderTransaction).toBeVisible();
533
+ console.log(`Transaction acessada: ${nomeTransaction}`);
534
+ if (traceExpandido === 'Não') {
535
+ try {
536
+ await test$1.expect(this.locatorExpandirTrace).toBeVisible({
537
+ timeout: 120000
538
+ });
539
+ } catch (_) {
540
+ console.log('Botão expandir trace não ficou visível. Recarregando...');
541
+ await this.page.reload();
542
+ await this.page.waitForLoadState('networkidle');
543
+ await test$1.expect(this.locatorHeaderTransaction).toBeVisible();
544
+ await test$1.expect(this.locatorExpandirTrace).toBeVisible({
545
+ timeout: 120000
546
+ });
547
+ }
548
+ await this.locatorExpandirTrace.click();
549
+ } else {
550
+ const traceTitle = this.page.locator('h5.euiTitle').filter({
551
+ hasText: nomeTransaction,
552
+ exact: true
553
+ }).first();
554
+ await test$1.expect(traceTitle).toBeVisible({
555
+ timeout: 120000
556
+ });
557
+ console.log(`Título do trace "${nomeTransaction}" validado`);
558
+ }
559
+ await this.comunsUtils.aguardarAte(async () => !(await this.page.getByRole('progressbar').first().isVisible()), {
560
+ timeout: 1000
561
+ });
562
+ }
563
+
564
+ /**
565
+ * Navega entre abas/seções do APM.
566
+ * Se nomeAba === 'Services', aguarda a resposta do endpoint de serviços.
567
+ * @param {string} nomeAba - Ex: 'Services', 'bridge'
568
+ */
569
+ async navegarTelaAPM(nomeAba) {
570
+ const link = this.page.getByRole('link', {
571
+ name: nomeAba,
572
+ exact: true
573
+ });
574
+ if (nomeAba === 'Services') {
575
+ const promise = this.page.waitForResponse(r => r.url().includes('services') && r.status() === 200);
576
+ await test$1.expect(link).toBeVisible();
577
+ await link.click();
578
+ await Promise.all([promise]);
579
+ } else {
580
+ await test$1.expect(link).toBeVisible();
581
+ await link.click();
582
+ await this.page.waitForLoadState('networkidle');
583
+ }
584
+ }
585
+ }
586
+
587
+ const CSS_VISAOCOMPLETA_TABLE = 'table.css-5cr14k';
588
+ const DATATESTID_PANELDATAERROR = 'data-testid Panel data error message';
589
+ const DATATESTID_PANELCONTENT = 'data-testid panel content';
590
+ const DATATESTID_PANELMENUITEMVIEW = 'data-testid Panel menu item View';
591
+ const DATATESTID_BACKDASHBOARD = 'data-testid Back to dashboard button';
592
+
593
+ /**
594
+ * Page Object para interações com o Grafana (painéis RabbitMQ e Kubernetes).
595
+ */
596
+ class GrafanaUtils {
597
+ /**
598
+ * @param {import('@playwright/test').Page} page
599
+ */
600
+ constructor(page) {
601
+ this.page = page;
602
+ this.comunsUtils = new ComunsUtils(page);
603
+ this.locatorPanelDataError = page.getByTestId(DATATESTID_PANELDATAERROR);
604
+ this.locatorTotalUnackedMessagesPanel = page.getByTestId('data-testid Panel header Total/unacked messages').getByTestId(DATATESTID_PANELCONTENT);
605
+ this.locatorPublishedAckedRatesPanel = page.getByTestId('data-testid Panel header Published/Acked messages rates').getByTestId(DATATESTID_PANELCONTENT);
606
+ this.locatorTabelaVisaoCompletaTable = page.locator(CSS_VISAOCOMPLETA_TABLE);
607
+ this.locatorTotalUnackedMaxCell = this.locatorTotalUnackedMessagesPanel.getByRole('cell', {
608
+ name: 'Max'
609
+ });
610
+ this.locatorVisaoCompletaButton = page.getByTestId(DATATESTID_PANELMENUITEMVIEW);
611
+ this.locatorVoltarDashBoardButton = page.getByTestId(DATATESTID_BACKDASHBOARD);
612
+ this.locatorVersoesServicoPanel = page.getByTestId('data-testid Panel header Images').getByTestId(DATATESTID_PANELCONTENT);
613
+ this.locatorVersoesServicosVisaoCompletaPanel = page.getByTestId('data-testid Panel header Images');
614
+ }
615
+
616
+ /**
617
+ * Navega para a URL do Grafana e fecha o "Dock menu" se visível.
618
+ * @param {string} url - URL do dashboard Grafana
619
+ */
620
+ async acessaTelaGrafana(url) {
621
+ await this.page.goto(url);
622
+ await this.page.waitForLoadState('networkidle');
623
+ const dockMenu = this.page.getByRole('button', {
624
+ name: 'Dock menu'
625
+ });
626
+ if (await dockMenu.isVisible()) {
627
+ await dockMenu.click();
628
+ console.log('Dock menu fechado');
629
+ }
630
+ console.log('Grafana carregado com sucesso');
631
+ }
632
+
633
+ /**
634
+ * Expande um painel colapsado no Grafana, validando que o conteúdo ficou visível.
635
+ * @param {string} nomePainel - Nome da row/painel a expandir
636
+ * @param {string} painelExpandido - Nome do painel filho para validação de visibilidade
637
+ */
638
+ async expandirPainel(nomePainel, painelExpandido) {
639
+ const painel = this.page.getByTestId(`data-testid dashboard-row-title-${nomePainel}`);
640
+ await test$1.expect(painel).toBeVisible();
641
+ const painelConteudo = this.page.getByTestId(`data-testid Panel header ${painelExpandido}`).getByTestId(DATATESTID_PANELCONTENT);
642
+ const isVisible = await painelConteudo.isVisible().catch(() => false);
643
+ if (!isVisible) {
644
+ await painel.click();
645
+ console.log(`Painel "${nomePainel}" expandido`);
646
+ const errorCount = await this.locatorPanelDataError.count();
647
+ if (errorCount > 0) {
648
+ const relevantErrors = this.locatorPanelDataError.filter({
649
+ hasNot: this.page.getByTestId('data-testid Panel header Restart Pods Prod')
650
+ });
651
+ if ((await relevantErrors.count()) > 0) {
652
+ await test$1.expect(relevantErrors.first()).toBeHidden();
653
+ }
654
+ }
655
+ await test$1.expect(painelConteudo).toBeVisible();
656
+ } else {
657
+ console.log(`Painel "${nomePainel}" já expandido`);
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Valida que o dashboard RabbitMQ foi carregado com os painéis esperados.
663
+ */
664
+ async validarGrafanaCarregado() {
665
+ await test$1.expect(this.locatorTotalUnackedMessagesPanel).toBeVisible();
666
+ await test$1.expect(this.locatorPublishedAckedRatesPanel).toBeVisible();
667
+ }
668
+
669
+ /**
670
+ * Clica na célula Max da tabela para ordenar pelo total de mensagens.
671
+ */
672
+ async ordenarMensagensTotal() {
673
+ await test$1.expect(this.locatorTotalUnackedMaxCell).toBeVisible();
674
+ await this.locatorTotalUnackedMaxCell.click();
675
+ }
676
+
677
+ /**
678
+ * Abre a visão completa (View) de uma seção do Grafana via hover no header + clique no menu.
679
+ * Faz scroll automático até o painel se necessário.
680
+ * @param {string} nomeSecao - Ex: 'Total/unacked messages', 'Images', 'Pods memory usage'
681
+ */
682
+ async acessarVisaoCompleta(nomeSecao) {
683
+ const locatorMenu = this.page.getByTestId(`data-testid Panel menu ${nomeSecao}`);
684
+ const locatorHeader = this.page.getByTestId(`data-testid Panel header ${nomeSecao}`).getByTestId('header-container');
685
+ let isVisible = false;
686
+ let scrollAttempts = 0;
687
+ while (!isVisible && scrollAttempts < 10) {
688
+ try {
689
+ await locatorHeader.waitFor({
690
+ state: 'visible',
691
+ timeout: 2000
692
+ });
693
+ isVisible = true;
694
+ } catch (_) {
695
+ const posicaoAnterior = await this.page.evaluate(() => window.scrollY);
696
+ await this.page.evaluate(() => window.scrollBy(0, 500));
697
+ await this.comunsUtils.aguardarAte(() => this.page.evaluate(anterior => window.scrollY !== anterior, posicaoAnterior), {
698
+ intervalo: 50,
699
+ timeout: 300
700
+ });
701
+ scrollAttempts++;
702
+ }
703
+ }
704
+ await locatorHeader.hover();
705
+ await locatorMenu.waitFor({
706
+ state: 'visible'
707
+ });
708
+ await locatorMenu.scrollIntoViewIfNeeded();
709
+ await test$1.expect(locatorMenu).toBeVisible();
710
+ await locatorMenu.click();
711
+ await test$1.expect(this.locatorVisaoCompletaButton).toBeVisible();
712
+ await this.locatorVisaoCompletaButton.click();
713
+ await this.page.waitForLoadState('networkidle');
714
+ }
715
+
716
+ /**
717
+ * Volta ao dashboard principal após visão completa.
718
+ */
719
+ async voltarParaDashboard() {
720
+ await test$1.expect(this.locatorVoltarDashBoardButton).toBeVisible();
721
+ await this.locatorVoltarDashBoardButton.click();
722
+ await test$1.expect(this.locatorVoltarDashBoardButton).toBeHidden();
723
+ await this.comunsUtils.aguardarAte(() => this.page.getByTestId(DATATESTID_PANELCONTENT).first().isVisible(), {
724
+ timeout: 1000
725
+ });
726
+ }
727
+ }
728
+
729
+ exports.frame = void 0;
730
+
731
+ /**
732
+ * Fixture Playwright estendido que injeta os page objects do core no contexto `page`.
733
+ *
734
+ * Disponível em cada test via:
735
+ * page.comunsUtils → ComunsUtils (login, screenshots, combinarPrints, gerarUrl*)
736
+ * page.grafanaUtils → GrafanaUtils (rabbit, kubernetes)
737
+ * page.apmUtils → ApmUtils (serviços, transactions, traces)
738
+ *
739
+ * @example
740
+ * import { test, expect } from 'automacao-core-carga-back/playwright';
741
+ *
742
+ * test('meu teste', async ({ page }) => {
743
+ * await page.comunsUtils.loginApm(APM_ERPXUI);
744
+ * await page.apmUtils.acessaTelaApm(url);
745
+ * });
746
+ */
747
+ const test = test$1.test.extend({
748
+ page: async ({
749
+ page
750
+ }, use) => {
751
+ page['comunsUtils'] = new ComunsUtils(page);
752
+ page['grafanaUtils'] = new GrafanaUtils(page);
753
+ page['apmUtils'] = new ApmUtils(page);
754
+ exports.frame = page.frameLocator('iframe[name="ci"]');
755
+ await use(page);
756
+ }
757
+ });
758
+
759
+ Object.defineProperty(exports, 'expect', {
760
+ enumerable: true,
761
+ get: function () { return test$1.expect; }
762
+ });
763
+ exports.ApmUtils = ApmUtils;
764
+ exports.ComunsUtils = ComunsUtils;
765
+ exports.GrafanaUtils = GrafanaUtils;
766
+ exports.test = test;