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