xref: /aosp_15_r20/external/pigweed/pw_web/log-viewer/test/log-viewer.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 '../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