xref: /aosp_15_r20/external/pigweed/pw_web/log-viewer/test/log-source.test.js (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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 { spy, match } from 'sinon';
17import { LogSource } from '../src/log-source';
18import { BrowserLogSource } from '../src/custom/browser-log-source';
19import { Level } from '../src/shared/interfaces';
20
21describe('log-source', () => {
22  let logSourceA, logSourceB;
23  const logEntry = {
24    level: 'INFO',
25    timestamp: new Date(Date.now()),
26    fields: [{ key: 'message', value: 'Log message' }],
27  };
28
29  beforeEach(() => {
30    logSourceA = new LogSource('Log Source A');
31    logSourceB = new LogSource('Log Source B');
32  });
33
34  afterEach(() => {
35    logSourceA = null;
36    logSourceB = null;
37  });
38
39  it('emits events to registered listeners', () => {
40    const eventType = 'log-entry';
41    let receivedData = null;
42
43    const listener = (event) => {
44      receivedData = event.data;
45    };
46
47    logSourceA.addEventListener(eventType, listener);
48    logSourceA.publishLogEntry(logEntry);
49
50    expect(receivedData).to.equal(logEntry);
51  });
52
53  it("logs aren't dropped at high read frequencies", async () => {
54    const numLogs = 10;
55    const logEntries = [];
56    const eventType = 'log-entry';
57    const listener = () => {
58      // Simulate a slow listener
59      return new Promise((resolve) => {
60        setTimeout(() => {
61          resolve();
62        }, 100);
63      });
64    };
65
66    logSourceA.addEventListener(eventType, listener);
67
68    const emittedLogs = [];
69
70    for (let i = 0; i < numLogs; i++) {
71      logEntries.push(logEntry);
72
73      await logSourceA.publishLogEntry(logEntry);
74      emittedLogs.push(logEntry);
75    }
76
77    await new Promise((resolve) => setTimeout(resolve, 200));
78
79    expect(emittedLogs).to.deep.equal(logEntries);
80  });
81
82  it('throws an error for incorrect log entry structure', async () => {
83    const incorrectLogEntry = {
84      fields: [{ key: 'message', value: 'Log entry without timestamp' }],
85    };
86
87    try {
88      await logSourceA.publishLogEntry(incorrectLogEntry);
89    } catch (error) {
90      expect(error.message).to.equal('Invalid log entry structure');
91    }
92  });
93
94  it('converts severity fields to level', async () => {
95    const severityLogEntry = {
96      severity: 'INFO',
97      timestamp: new Date(Date.now()),
98      fields: [
99        { key: 'message', value: 'Log message' },
100        { key: 'severity', value: 'INFO' },
101      ],
102    };
103
104    const eventType = 'log-entry';
105    let receivedData = null;
106
107    const listener = (event) => {
108      receivedData = event.data;
109    };
110    logSourceA.addEventListener(eventType, listener);
111    logSourceA.publishLogEntry(severityLogEntry);
112    expect(receivedData.severity).to.equal(undefined);
113    expect(receivedData.level).to.equal('INFO');
114    expect(receivedData.fields[2].key).to.equal('level');
115  });
116});
117
118describe('browser-log-source', () => {
119  let browserLogSource;
120  let originalConsoleMethods;
121
122  beforeEach(() => {
123    originalConsoleMethods = {
124      log: console.log,
125      info: console.info,
126      warn: console.warn,
127      error: console.error,
128      debug: console.debug,
129    };
130    browserLogSource = new BrowserLogSource();
131    browserLogSource.start();
132    browserLogSource.publishLogEntry = spy();
133  });
134
135  afterEach(() => {
136    browserLogSource.stop();
137
138    console.log = originalConsoleMethods.log;
139    console.info = originalConsoleMethods.info;
140    console.warn = originalConsoleMethods.warn;
141    console.error = originalConsoleMethods.error;
142    console.debug = originalConsoleMethods.debug;
143  });
144
145  it('captures and formats console.log messages with substitutions correctly', () => {
146    browserLogSource.publishLogEntry.resetHistory();
147
148    console.log("Hello, %s. You've called me %d times.", 'Alice', 5);
149    const expectedMessage = "Hello, Alice. You've called me 5 times.";
150
151    expect(browserLogSource.publishLogEntry.calledOnce).to.be.true;
152
153    const callArgs = browserLogSource.publishLogEntry.getCall(0).args[0];
154    expect(callArgs.level).to.equal(Level.INFO);
155
156    const messageField = callArgs.fields.find(
157      (field) => field.key === 'message',
158    );
159    expect(messageField).to.exist;
160    expect(messageField.value).to.equal(expectedMessage);
161  });
162
163  ['log', 'info', 'warn', 'error', 'debug'].forEach((method) => {
164    it(`captures and formats console.${method} messages`, () => {
165      const expectedLevel = mapMethodToLevel(method);
166
167      // For test, log-source.test.js:XXX needs to match next line number
168      console[method]('Test message (%s)', method);
169      expect(browserLogSource.publishLogEntry).to.have.been.calledWithMatch({
170        timestamp: match.instanceOf(Date),
171        level: expectedLevel,
172        fields: [
173          { key: 'level', value: expectedLevel },
174          { key: 'time', value: match.typeOf('string') },
175          { key: 'message', value: `Test message (${method})` },
176          { key: 'file', value: 'log-source.test.js:168' },
177        ],
178      });
179    });
180  });
181
182  function mapMethodToLevel(method) {
183    switch (method) {
184      case 'log':
185      case 'info':
186        return Level.INFO;
187      case 'warn':
188        return Level.WARNING;
189      case 'error':
190        return Level.ERROR;
191      case 'debug':
192        return Level.DEBUG;
193      default:
194        return Level.INFO;
195    }
196  }
197
198  it('captures and formats multiple arguments correctly', () => {
199    console.log('This is a test', 42, { type: 'answer' });
200
201    const expectedMessage = 'This is a test 42 {"type":"answer"}';
202
203    expect(browserLogSource.publishLogEntry.calledOnce).to.be.true;
204    const callArgs = browserLogSource.publishLogEntry.getCall(0).args[0];
205    expect(callArgs.level).to.equal(Level.INFO);
206
207    const messageField = callArgs.fields.find(
208      (field) => field.key === 'message',
209    );
210    expect(messageField).to.exist;
211    expect(messageField.value).to.equal(expectedMessage);
212  });
213
214  it('restores original console methods after stop is called', () => {
215    browserLogSource.stop();
216    expect(console.log).to.equal(originalConsoleMethods.log);
217    expect(console.info).to.equal(originalConsoleMethods.info);
218    expect(console.warn).to.equal(originalConsoleMethods.warn);
219    expect(console.error).to.equal(originalConsoleMethods.error);
220    expect(console.debug).to.equal(originalConsoleMethods.debug);
221  });
222});
223