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