1// Copyright 2024 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 { assert } from '@open-wc/testing'; 16import { MockLogSource } from '../src/custom/mock-log-source'; 17import { createLogViewer } from '../src/createLogViewer'; 18import { LocalStateStorage, StateService } from '../src/shared/state'; 19import { NodeType, Orientation, ViewNode } from '../src/shared/view-node'; 20 21function setUpLogViewer(logSources) { 22 const destroyLogViewer = createLogViewer(logSources, document.body); 23 const logViewer = document.querySelector('log-viewer'); 24 return { logSources, destroyLogViewer, logViewer }; 25} 26 27// Handle benign ResizeObserver error caused by custom log viewer initialization 28// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors 29function handleResizeObserverError() { 30 const e = window.onerror; 31 window.onerror = function (err) { 32 if ( 33 err === 'ResizeObserver loop completed with undelivered notifications.' 34 ) { 35 console.warn( 36 'Ignored: ResizeObserver loop completed with undelivered notifications.', 37 ); 38 return false; 39 } else { 40 return e(...arguments); 41 } 42 }; 43} 44 45describe('log-view', () => { 46 let logSources; 47 let destroyLogViewer; 48 let logViewer; 49 let stateService; 50 let mockColumnData; 51 let mockState; 52 53 async function getLogViews() { 54 const logViewerEl = document.querySelector('log-viewer'); 55 await logViewerEl.updateComplete; 56 await new Promise((resolve) => setTimeout(resolve, 100)); 57 const logViews = logViewerEl.shadowRoot.querySelectorAll('log-view'); 58 return logViews; 59 } 60 61 describe('state', () => { 62 beforeEach(() => { 63 mockColumnData = [ 64 { 65 fieldName: 'test', 66 characterLength: 0, 67 manualWidth: null, 68 isVisible: false, 69 }, 70 { 71 fieldName: 'foo', 72 characterLength: 0, 73 manualWidth: null, 74 isVisible: true, 75 }, 76 { 77 fieldName: 'bar', 78 characterLength: 0, 79 manualWidth: null, 80 isVisible: false, 81 }, 82 ]; 83 84 mockState = { 85 rootNode: new ViewNode({ 86 type: NodeType.Split, 87 orientation: Orientation.Horizontal, 88 children: [ 89 new ViewNode({ 90 searchText: 'hello', 91 logViewId: 'child-node-1', 92 type: NodeType.View, 93 columnData: mockColumnData, 94 }), 95 new ViewNode({ 96 searchText: 'world', 97 logViewId: 'child-node-2', 98 type: NodeType.View, 99 columnData: mockColumnData, 100 }), 101 ], 102 }), 103 }; 104 105 stateService = new StateService(new LocalStateStorage()); 106 stateService.saveState(mockState); 107 handleResizeObserverError(); 108 }); 109 110 afterEach(() => { 111 destroyLogViewer(); 112 }); 113 114 it('should default to single view when state is cleared', async () => { 115 window.localStorage.clear(); 116 117 ({ logSources, destroyLogViewer, logViewer } = setUpLogViewer([ 118 new MockLogSource(), 119 ])); 120 const logViews = await getLogViews(); 121 122 assert.lengthOf(logViews, 1); 123 }); 124 125 it('should populate correct number of views from state', async () => { 126 ({ logSources, destroyLogViewer, logViewer } = setUpLogViewer([ 127 new MockLogSource(), 128 ])); 129 130 const logViews = await getLogViews(); 131 assert.lengthOf(logViews, 2); 132 }); 133 }); 134 135 describe('sources', () => { 136 before(() => { 137 window.localStorage.clear(); 138 ({ logSources, destroyLogViewer, logViewer } = setUpLogViewer([ 139 new MockLogSource('Source 1'), 140 new MockLogSource('Source 2'), 141 ])); 142 }); 143 144 after(() => { 145 destroyLogViewer(); 146 window.localStorage.clear(); 147 }); 148 149 it('registers a new source upon receiving its first log entry', async () => { 150 const logSource1 = logSources[0]; 151 152 logSource1.publishLogEntry({ 153 timestamp: new Date(), 154 fields: [{ key: 'message', value: 'Message from Source 1' }], 155 }); 156 157 await logViewer.updateComplete; 158 await new Promise((resolve) => setTimeout(resolve, 100)); 159 160 const logViews = await getLogViews(); 161 const sources = logViews[0]?.sources; 162 const sourceNames = Array.from(sources.values()).map( 163 (source) => source.name, 164 ); 165 166 assert.include( 167 sourceNames, 168 'Source 1', 169 'New source should be registered after emitting its first log entry', 170 ); 171 }); 172 173 it('keeps a record of multiple log sources', async () => { 174 const logSource2 = logSources[1]; 175 176 logSource2.publishLogEntry({ 177 timestamp: new Date(), 178 fields: [{ key: 'message', value: 'Message from Source 2' }], 179 }); 180 181 await logViewer.updateComplete; 182 await new Promise((resolve) => setTimeout(resolve, 100)); 183 184 const logViews = await getLogViews(); 185 const sources = logViews[0]?.sources; 186 const sourceNames = Array.from(sources.values()).map( 187 (source) => source.name, 188 ); 189 190 assert.includeMembers( 191 sourceNames, 192 ['Source 1', 'Source 2'], 193 'Both sources should be present', 194 ); 195 }); 196 }); 197}); 198