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