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