1*61c4878aSAndroid Build Coastguard Worker// Copyright 2023 The Pigweed Authors 2*61c4878aSAndroid Build Coastguard Worker// 3*61c4878aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4*61c4878aSAndroid Build Coastguard Worker// use this file except in compliance with the License. You may obtain a copy of 5*61c4878aSAndroid Build Coastguard Worker// the License at 6*61c4878aSAndroid Build Coastguard Worker// 7*61c4878aSAndroid Build Coastguard Worker// https://www.apache.org/licenses/LICENSE-2.0 8*61c4878aSAndroid Build Coastguard Worker// 9*61c4878aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*61c4878aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11*61c4878aSAndroid Build Coastguard Worker// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12*61c4878aSAndroid Build Coastguard Worker// License for the specific language governing permissions and limitations under 13*61c4878aSAndroid Build Coastguard Worker// the License. 14*61c4878aSAndroid Build Coastguard Worker 15*61c4878aSAndroid Build Coastguard Workerimport { expect } from '@open-wc/testing'; 16*61c4878aSAndroid Build Coastguard Workerimport '../src/components/log-viewer'; 17*61c4878aSAndroid Build Coastguard Workerimport { MockLogSource } from '../src/custom/mock-log-source'; 18*61c4878aSAndroid Build Coastguard Workerimport { createLogViewer } from '../src/createLogViewer'; 19*61c4878aSAndroid Build Coastguard Worker 20*61c4878aSAndroid Build Coastguard Worker// Initialize the log viewer component with a mock log source 21*61c4878aSAndroid Build Coastguard Workerfunction setUpLogViewer(columnOrder) { 22*61c4878aSAndroid Build Coastguard Worker const mockLogSource = new MockLogSource(); 23*61c4878aSAndroid Build Coastguard Worker const destroyLogViewer = createLogViewer(mockLogSource, document.body, { 24*61c4878aSAndroid Build Coastguard Worker columnOrder, 25*61c4878aSAndroid Build Coastguard Worker }); 26*61c4878aSAndroid Build Coastguard Worker const logViewer = document.querySelector('log-viewer'); 27*61c4878aSAndroid Build Coastguard Worker return { mockLogSource, destroyLogViewer, logViewer }; 28*61c4878aSAndroid Build Coastguard Worker} 29*61c4878aSAndroid Build Coastguard Worker 30*61c4878aSAndroid Build Coastguard Worker// Handle benign ResizeObserver error caused by custom log viewer initialization 31*61c4878aSAndroid Build Coastguard Worker// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors 32*61c4878aSAndroid Build Coastguard Workerfunction handleResizeObserverError() { 33*61c4878aSAndroid Build Coastguard Worker const e = window.onerror; 34*61c4878aSAndroid Build Coastguard Worker window.onerror = function (err) { 35*61c4878aSAndroid Build Coastguard Worker if ( 36*61c4878aSAndroid Build Coastguard Worker err === 'ResizeObserver loop completed with undelivered notifications.' 37*61c4878aSAndroid Build Coastguard Worker ) { 38*61c4878aSAndroid Build Coastguard Worker console.warn( 39*61c4878aSAndroid Build Coastguard Worker 'Ignored: ResizeObserver loop completed with undelivered notifications.', 40*61c4878aSAndroid Build Coastguard Worker ); 41*61c4878aSAndroid Build Coastguard Worker return false; 42*61c4878aSAndroid Build Coastguard Worker } else { 43*61c4878aSAndroid Build Coastguard Worker return e(...arguments); 44*61c4878aSAndroid Build Coastguard Worker } 45*61c4878aSAndroid Build Coastguard Worker }; 46*61c4878aSAndroid Build Coastguard Worker} 47*61c4878aSAndroid Build Coastguard Worker 48*61c4878aSAndroid Build Coastguard Worker/** 49*61c4878aSAndroid Build Coastguard Worker * Checks if the table header cells in the rendered log viewer match the given 50*61c4878aSAndroid Build Coastguard Worker * expected column names. 51*61c4878aSAndroid Build Coastguard Worker */ 52*61c4878aSAndroid Build Coastguard Workerfunction checkTableHeaderCells(table, expectedColumnNames) { 53*61c4878aSAndroid Build Coastguard Worker const tableHeaderRow = table.querySelector('thead tr'); 54*61c4878aSAndroid Build Coastguard Worker const tableHeaderCells = tableHeaderRow.querySelectorAll('th'); 55*61c4878aSAndroid Build Coastguard Worker 56*61c4878aSAndroid Build Coastguard Worker expect(tableHeaderCells).to.have.lengthOf(expectedColumnNames.length); 57*61c4878aSAndroid Build Coastguard Worker 58*61c4878aSAndroid Build Coastguard Worker for (let i = 0; i < tableHeaderCells.length; i++) { 59*61c4878aSAndroid Build Coastguard Worker const columnName = tableHeaderCells[i].textContent.trim(); 60*61c4878aSAndroid Build Coastguard Worker expect(columnName).to.equal(expectedColumnNames[i]); 61*61c4878aSAndroid Build Coastguard Worker } 62*61c4878aSAndroid Build Coastguard Worker} 63*61c4878aSAndroid Build Coastguard Worker 64*61c4878aSAndroid Build Coastguard Worker/** 65*61c4878aSAndroid Build Coastguard Worker * Checks if the table body cells in the log viewer match the values of the given log entry objects. 66*61c4878aSAndroid Build Coastguard Worker */ 67*61c4878aSAndroid Build Coastguard Workerfunction checkTableBodyCells(table, logEntries) { 68*61c4878aSAndroid Build Coastguard Worker const tableHeaderRow = table.querySelector('thead tr'); 69*61c4878aSAndroid Build Coastguard Worker const tableHeaderCells = tableHeaderRow.querySelectorAll('th'); 70*61c4878aSAndroid Build Coastguard Worker const tableBody = table.querySelector('tbody'); 71*61c4878aSAndroid Build Coastguard Worker const tableRows = tableBody.querySelectorAll('tr'); 72*61c4878aSAndroid Build Coastguard Worker const fieldKeys = Array.from(tableHeaderCells).map((cell) => 73*61c4878aSAndroid Build Coastguard Worker cell.textContent.trim(), 74*61c4878aSAndroid Build Coastguard Worker ); 75*61c4878aSAndroid Build Coastguard Worker 76*61c4878aSAndroid Build Coastguard Worker // Iterate through each row and cell in the table body 77*61c4878aSAndroid Build Coastguard Worker tableRows.forEach((row, rowIndex) => { 78*61c4878aSAndroid Build Coastguard Worker const cells = row.querySelectorAll('td'); 79*61c4878aSAndroid Build Coastguard Worker const logEntry = logEntries[rowIndex]; 80*61c4878aSAndroid Build Coastguard Worker 81*61c4878aSAndroid Build Coastguard Worker cells.forEach((cell, cellIndex) => { 82*61c4878aSAndroid Build Coastguard Worker const fieldKey = fieldKeys[cellIndex]; 83*61c4878aSAndroid Build Coastguard Worker const cellContent = cell.textContent.trim(); 84*61c4878aSAndroid Build Coastguard Worker 85*61c4878aSAndroid Build Coastguard Worker if (logEntry.fields.some((field) => field.key === fieldKey)) { 86*61c4878aSAndroid Build Coastguard Worker const fieldValue = logEntry.fields.find( 87*61c4878aSAndroid Build Coastguard Worker (field) => field.key === fieldKey, 88*61c4878aSAndroid Build Coastguard Worker ).value; 89*61c4878aSAndroid Build Coastguard Worker expect(cellContent).to.equal(String(fieldValue)); 90*61c4878aSAndroid Build Coastguard Worker } else { 91*61c4878aSAndroid Build Coastguard Worker // Cell should be empty for missing fields 92*61c4878aSAndroid Build Coastguard Worker expect(cellContent).to.equal(''); 93*61c4878aSAndroid Build Coastguard Worker } 94*61c4878aSAndroid Build Coastguard Worker }); 95*61c4878aSAndroid Build Coastguard Worker }); 96*61c4878aSAndroid Build Coastguard Worker} 97*61c4878aSAndroid Build Coastguard Worker 98*61c4878aSAndroid Build Coastguard Workerasync function appendLogsAndWait(logViewer, logEntries) { 99*61c4878aSAndroid Build Coastguard Worker const currentLogs = logViewer.logs || []; 100*61c4878aSAndroid Build Coastguard Worker logViewer.logs = [...currentLogs, ...logEntries]; 101*61c4878aSAndroid Build Coastguard Worker 102*61c4878aSAndroid Build Coastguard Worker await logViewer.updateComplete; 103*61c4878aSAndroid Build Coastguard Worker await new Promise((resolve) => setTimeout(resolve, 100)); 104*61c4878aSAndroid Build Coastguard Worker} 105*61c4878aSAndroid Build Coastguard Worker 106*61c4878aSAndroid Build Coastguard Workerdescribe('log-viewer', () => { 107*61c4878aSAndroid Build Coastguard Worker let mockLogSource; 108*61c4878aSAndroid Build Coastguard Worker let destroyLogViewer; 109*61c4878aSAndroid Build Coastguard Worker let logViewer; 110*61c4878aSAndroid Build Coastguard Worker 111*61c4878aSAndroid Build Coastguard Worker beforeEach(() => { 112*61c4878aSAndroid Build Coastguard Worker window.localStorage.clear(); 113*61c4878aSAndroid Build Coastguard Worker ({ mockLogSource, destroyLogViewer, logViewer } = setUpLogViewer([''])); 114*61c4878aSAndroid Build Coastguard Worker handleResizeObserverError(); 115*61c4878aSAndroid Build Coastguard Worker }); 116*61c4878aSAndroid Build Coastguard Worker 117*61c4878aSAndroid Build Coastguard Worker afterEach(() => { 118*61c4878aSAndroid Build Coastguard Worker mockLogSource.stop(); 119*61c4878aSAndroid Build Coastguard Worker destroyLogViewer(); 120*61c4878aSAndroid Build Coastguard Worker }); 121*61c4878aSAndroid Build Coastguard Worker 122*61c4878aSAndroid Build Coastguard Worker it('should generate table columns properly with correctly-structured logs', async () => { 123*61c4878aSAndroid Build Coastguard Worker const logEntry1 = { 124*61c4878aSAndroid Build Coastguard Worker timestamp: new Date(), 125*61c4878aSAndroid Build Coastguard Worker fields: [ 126*61c4878aSAndroid Build Coastguard Worker { key: 'source', value: 'application' }, 127*61c4878aSAndroid Build Coastguard Worker { key: 'timestamp', value: '2023-11-13T23:05:16.520Z' }, 128*61c4878aSAndroid Build Coastguard Worker { key: 'message', value: 'Log entry 1' }, 129*61c4878aSAndroid Build Coastguard Worker ], 130*61c4878aSAndroid Build Coastguard Worker }; 131*61c4878aSAndroid Build Coastguard Worker 132*61c4878aSAndroid Build Coastguard Worker const logEntry2 = { 133*61c4878aSAndroid Build Coastguard Worker timestamp: new Date(), 134*61c4878aSAndroid Build Coastguard Worker fields: [ 135*61c4878aSAndroid Build Coastguard Worker { key: 'source', value: 'server' }, 136*61c4878aSAndroid Build Coastguard Worker { key: 'timestamp', value: '2023-11-13T23:10:00.000Z' }, 137*61c4878aSAndroid Build Coastguard Worker { key: 'message', value: 'Log entry 2' }, 138*61c4878aSAndroid Build Coastguard Worker { key: 'user', value: 'Alice' }, 139*61c4878aSAndroid Build Coastguard Worker ], 140*61c4878aSAndroid Build Coastguard Worker }; 141*61c4878aSAndroid Build Coastguard Worker 142*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, [logEntry1, logEntry2]); 143*61c4878aSAndroid Build Coastguard Worker 144*61c4878aSAndroid Build Coastguard Worker const { table } = getLogViewerElements(logViewer); 145*61c4878aSAndroid Build Coastguard Worker const expectedColumnNames = ['source', 'timestamp', 'user', 'message']; 146*61c4878aSAndroid Build Coastguard Worker checkTableHeaderCells(table, expectedColumnNames); 147*61c4878aSAndroid Build Coastguard Worker }); 148*61c4878aSAndroid Build Coastguard Worker 149*61c4878aSAndroid Build Coastguard Worker it('displays the correct number of logs', async () => { 150*61c4878aSAndroid Build Coastguard Worker const numLogs = 5; 151*61c4878aSAndroid Build Coastguard Worker const logEntries = []; 152*61c4878aSAndroid Build Coastguard Worker 153*61c4878aSAndroid Build Coastguard Worker for (let i = 0; i < numLogs; i++) { 154*61c4878aSAndroid Build Coastguard Worker const logEntry = mockLogSource.readLogEntryFromHost(); 155*61c4878aSAndroid Build Coastguard Worker logEntries.push(logEntry); 156*61c4878aSAndroid Build Coastguard Worker } 157*61c4878aSAndroid Build Coastguard Worker 158*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, logEntries); 159*61c4878aSAndroid Build Coastguard Worker 160*61c4878aSAndroid Build Coastguard Worker const { table } = getLogViewerElements(logViewer); 161*61c4878aSAndroid Build Coastguard Worker const tableRows = table.querySelectorAll('tbody tr'); 162*61c4878aSAndroid Build Coastguard Worker 163*61c4878aSAndroid Build Coastguard Worker expect(tableRows.length).to.equal(numLogs); 164*61c4878aSAndroid Build Coastguard Worker }); 165*61c4878aSAndroid Build Coastguard Worker 166*61c4878aSAndroid Build Coastguard Worker it('should display columns properly given varying log entry fields', async () => { 167*61c4878aSAndroid Build Coastguard Worker // Create log entries with differing fields 168*61c4878aSAndroid Build Coastguard Worker const logEntry1 = { 169*61c4878aSAndroid Build Coastguard Worker timestamp: new Date(), 170*61c4878aSAndroid Build Coastguard Worker fields: [ 171*61c4878aSAndroid Build Coastguard Worker { key: 'source', value: 'application' }, 172*61c4878aSAndroid Build Coastguard Worker { key: 'timestamp', value: '2023-11-13T23:05:16.520Z' }, 173*61c4878aSAndroid Build Coastguard Worker { key: 'message', value: 'Log entry 1' }, 174*61c4878aSAndroid Build Coastguard Worker ], 175*61c4878aSAndroid Build Coastguard Worker }; 176*61c4878aSAndroid Build Coastguard Worker 177*61c4878aSAndroid Build Coastguard Worker const logEntry2 = { 178*61c4878aSAndroid Build Coastguard Worker timestamp: new Date(), 179*61c4878aSAndroid Build Coastguard Worker fields: [ 180*61c4878aSAndroid Build Coastguard Worker { key: 'source', value: 'server' }, 181*61c4878aSAndroid Build Coastguard Worker { key: 'timestamp', value: '2023-11-13T23:10:00.000Z' }, 182*61c4878aSAndroid Build Coastguard Worker { key: 'message', value: 'Log entry 2' }, 183*61c4878aSAndroid Build Coastguard Worker { key: 'user', value: 'Alice' }, 184*61c4878aSAndroid Build Coastguard Worker ], 185*61c4878aSAndroid Build Coastguard Worker }; 186*61c4878aSAndroid Build Coastguard Worker 187*61c4878aSAndroid Build Coastguard Worker const logEntry3 = { 188*61c4878aSAndroid Build Coastguard Worker timestamp: new Date(), 189*61c4878aSAndroid Build Coastguard Worker fields: [ 190*61c4878aSAndroid Build Coastguard Worker { key: 'source', value: 'database' }, 191*61c4878aSAndroid Build Coastguard Worker { key: 'timestamp', value: '2023-11-13T23:15:00.000Z' }, 192*61c4878aSAndroid Build Coastguard Worker { key: 'description', value: 'Log entry 3' }, 193*61c4878aSAndroid Build Coastguard Worker ], 194*61c4878aSAndroid Build Coastguard Worker }; 195*61c4878aSAndroid Build Coastguard Worker 196*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, [logEntry1, logEntry2, logEntry3]); 197*61c4878aSAndroid Build Coastguard Worker 198*61c4878aSAndroid Build Coastguard Worker const { table } = getLogViewerElements(logViewer); 199*61c4878aSAndroid Build Coastguard Worker const expectedColumnNames = [ 200*61c4878aSAndroid Build Coastguard Worker 'source', 201*61c4878aSAndroid Build Coastguard Worker 'timestamp', 202*61c4878aSAndroid Build Coastguard Worker 'user', 203*61c4878aSAndroid Build Coastguard Worker 'description', 204*61c4878aSAndroid Build Coastguard Worker 'message', 205*61c4878aSAndroid Build Coastguard Worker ]; 206*61c4878aSAndroid Build Coastguard Worker 207*61c4878aSAndroid Build Coastguard Worker checkTableHeaderCells(table, expectedColumnNames); 208*61c4878aSAndroid Build Coastguard Worker 209*61c4878aSAndroid Build Coastguard Worker checkTableBodyCells(table, logViewer.logs); 210*61c4878aSAndroid Build Coastguard Worker }); 211*61c4878aSAndroid Build Coastguard Worker 212*61c4878aSAndroid Build Coastguard Worker it('should expose log view subcomponent(s) and properties', async () => { 213*61c4878aSAndroid Build Coastguard Worker await logViewer.updateComplete; 214*61c4878aSAndroid Build Coastguard Worker await new Promise((resolve) => setTimeout(resolve, 100)); 215*61c4878aSAndroid Build Coastguard Worker 216*61c4878aSAndroid Build Coastguard Worker const logView = logViewer.logViews; 217*61c4878aSAndroid Build Coastguard Worker expect(logView).to.have.lengthOf(1); 218*61c4878aSAndroid Build Coastguard Worker 219*61c4878aSAndroid Build Coastguard Worker logView[0].viewTitle = 'Test'; 220*61c4878aSAndroid Build Coastguard Worker expect(logView[0].viewTitle).to.equal('Test'); 221*61c4878aSAndroid Build Coastguard Worker }); 222*61c4878aSAndroid Build Coastguard Worker 223*61c4878aSAndroid Build Coastguard Worker describe('column order', async () => { 224*61c4878aSAndroid Build Coastguard Worker const logEntry1 = { 225*61c4878aSAndroid Build Coastguard Worker timestamp: new Date(), 226*61c4878aSAndroid Build Coastguard Worker fields: [ 227*61c4878aSAndroid Build Coastguard Worker { key: 'source', value: 'application' }, 228*61c4878aSAndroid Build Coastguard Worker { key: 'timestamp', value: '2023-11-13T23:05:16.520Z' }, 229*61c4878aSAndroid Build Coastguard Worker { key: 'message', value: 'Log entry 1' }, 230*61c4878aSAndroid Build Coastguard Worker ], 231*61c4878aSAndroid Build Coastguard Worker }; 232*61c4878aSAndroid Build Coastguard Worker 233*61c4878aSAndroid Build Coastguard Worker it('should generate table columns in defined order', async () => { 234*61c4878aSAndroid Build Coastguard Worker destroyLogViewer(); 235*61c4878aSAndroid Build Coastguard Worker ({ mockLogSource, destroyLogViewer, logViewer } = setUpLogViewer([ 236*61c4878aSAndroid Build Coastguard Worker 'timestamp', 237*61c4878aSAndroid Build Coastguard Worker ])); 238*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, [logEntry1]); 239*61c4878aSAndroid Build Coastguard Worker 240*61c4878aSAndroid Build Coastguard Worker const { table } = getLogViewerElements(logViewer); 241*61c4878aSAndroid Build Coastguard Worker const expectedColumnNames = ['timestamp', 'source', 'message']; 242*61c4878aSAndroid Build Coastguard Worker checkTableHeaderCells(table, expectedColumnNames); 243*61c4878aSAndroid Build Coastguard Worker }); 244*61c4878aSAndroid Build Coastguard Worker 245*61c4878aSAndroid Build Coastguard Worker it('removes duplicate columns in defined order', async () => { 246*61c4878aSAndroid Build Coastguard Worker destroyLogViewer(); 247*61c4878aSAndroid Build Coastguard Worker ({ mockLogSource, destroyLogViewer, logViewer } = setUpLogViewer([ 248*61c4878aSAndroid Build Coastguard Worker 'timestamp', 249*61c4878aSAndroid Build Coastguard Worker 'source', 250*61c4878aSAndroid Build Coastguard Worker 'timestamp', 251*61c4878aSAndroid Build Coastguard Worker ])); 252*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, [logEntry1]); 253*61c4878aSAndroid Build Coastguard Worker 254*61c4878aSAndroid Build Coastguard Worker const { table } = getLogViewerElements(logViewer); 255*61c4878aSAndroid Build Coastguard Worker const expectedColumnNames = ['timestamp', 'source', 'message']; 256*61c4878aSAndroid Build Coastguard Worker checkTableHeaderCells(table, expectedColumnNames); 257*61c4878aSAndroid Build Coastguard Worker }); 258*61c4878aSAndroid Build Coastguard Worker 259*61c4878aSAndroid Build Coastguard Worker it('orders columns if data is stored in state', async () => { 260*61c4878aSAndroid Build Coastguard Worker destroyLogViewer(); 261*61c4878aSAndroid Build Coastguard Worker ({ mockLogSource, destroyLogViewer, logViewer } = setUpLogViewer([ 262*61c4878aSAndroid Build Coastguard Worker 'timestamp', 263*61c4878aSAndroid Build Coastguard Worker ])); 264*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, [logEntry1]); 265*61c4878aSAndroid Build Coastguard Worker 266*61c4878aSAndroid Build Coastguard Worker destroyLogViewer(); 267*61c4878aSAndroid Build Coastguard Worker ({ mockLogSource, destroyLogViewer, logViewer } = setUpLogViewer([ 268*61c4878aSAndroid Build Coastguard Worker 'source', 269*61c4878aSAndroid Build Coastguard Worker 'timestamp', 270*61c4878aSAndroid Build Coastguard Worker ])); 271*61c4878aSAndroid Build Coastguard Worker await appendLogsAndWait(logViewer, [logEntry1]); 272*61c4878aSAndroid Build Coastguard Worker 273*61c4878aSAndroid Build Coastguard Worker const { table } = getLogViewerElements(logViewer); 274*61c4878aSAndroid Build Coastguard Worker const expectedColumnNames = ['source', 'timestamp', 'message']; 275*61c4878aSAndroid Build Coastguard Worker checkTableHeaderCells(table, expectedColumnNames); 276*61c4878aSAndroid Build Coastguard Worker }); 277*61c4878aSAndroid Build Coastguard Worker }); 278*61c4878aSAndroid Build Coastguard Worker}); 279*61c4878aSAndroid Build Coastguard Worker 280*61c4878aSAndroid Build Coastguard Workerfunction getLogViewerElements(logViewer) { 281*61c4878aSAndroid Build Coastguard Worker const logView = logViewer.shadowRoot.querySelector('log-view'); 282*61c4878aSAndroid Build Coastguard Worker const logList = logView.shadowRoot.querySelector('log-list'); 283*61c4878aSAndroid Build Coastguard Worker const table = logList.shadowRoot.querySelector('table'); 284*61c4878aSAndroid Build Coastguard Worker 285*61c4878aSAndroid Build Coastguard Worker return { logView, logList, table }; 286*61c4878aSAndroid Build Coastguard Worker} 287