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