LogTable.test.tsx309 lines · main
| 1 | import { screen, waitFor } from '@testing-library/react' |
| 2 | import userEvent from '@testing-library/user-event' |
| 3 | import dayjs from 'dayjs' |
| 4 | import customParseFormat from 'dayjs/plugin/customParseFormat' |
| 5 | import relativeTime from 'dayjs/plugin/relativeTime' |
| 6 | import timezone from 'dayjs/plugin/timezone' |
| 7 | import utc from 'dayjs/plugin/utc' |
| 8 | import { expect, test, vi } from 'vitest' |
| 9 | |
| 10 | import { LogTable } from '@/components/interfaces/Settings/Logs/LogTable' |
| 11 | import { customRender as render } from '@/tests/lib/custom-render' |
| 12 | |
| 13 | dayjs.extend(customParseFormat) |
| 14 | dayjs.extend(utc) |
| 15 | dayjs.extend(timezone) |
| 16 | dayjs.extend(relativeTime) |
| 17 | |
| 18 | vi.mock('next/router', () => import('next-router-mock')) |
| 19 | |
| 20 | vi.mock('react-data-grid', () => ({ |
| 21 | default: ({ columns, rows, renderers, role, headerRowHeight }: any) => ( |
| 22 | <div role={role ?? 'table'}> |
| 23 | {headerRowHeight !== 0 && ( |
| 24 | <div role="row"> |
| 25 | {columns.map((col: any, colIdx: number) => ( |
| 26 | <div key={colIdx} role="columnheader"> |
| 27 | {col.renderHeaderCell ? col.renderHeaderCell({}) : col.name} |
| 28 | </div> |
| 29 | ))} |
| 30 | </div> |
| 31 | )} |
| 32 | {rows.map((row: any, rowIdx: number) => ( |
| 33 | <div key={rowIdx} role="row"> |
| 34 | {columns.map((col: any, colIdx: number) => ( |
| 35 | <div key={colIdx} role="cell"> |
| 36 | {col.renderCell?.({ row, rowIdx, isCellSelected: false })} |
| 37 | </div> |
| 38 | ))} |
| 39 | </div> |
| 40 | ))} |
| 41 | {rows.length === 0 && renderers?.noRowsFallback} |
| 42 | </div> |
| 43 | ), |
| 44 | Row: ({ row, ...props }: any) => <div role="row" {...props} />, |
| 45 | })) |
| 46 | |
| 47 | const fakeMicroTimestamp = dayjs().unix() * 1000 |
| 48 | |
| 49 | const LOG_DATA = { |
| 50 | id: 'some-uuid', |
| 51 | timestamp: 1621323232312, |
| 52 | event_message: 'event message', |
| 53 | metadata: { |
| 54 | my_key: 'something_value', |
| 55 | }, |
| 56 | } |
| 57 | |
| 58 | test('can display log data', async () => { |
| 59 | render( |
| 60 | <> |
| 61 | <LogTable projectRef="default" data={[LOG_DATA]} /> |
| 62 | </> |
| 63 | ) |
| 64 | |
| 65 | await screen.findAllByText(LOG_DATA.timestamp) |
| 66 | }) |
| 67 | |
| 68 | test('Shows total results', async () => { |
| 69 | render(<LogTable projectRef="default" data={[LOG_DATA]} />) |
| 70 | |
| 71 | await screen.getByText(/results \(1\)/i) |
| 72 | }) |
| 73 | |
| 74 | test('can run if no queryType provided', async () => { |
| 75 | const mockRun = vi.fn() |
| 76 | |
| 77 | render( |
| 78 | <LogTable |
| 79 | projectRef="projectRef" |
| 80 | data={[ |
| 81 | { |
| 82 | id: 'some-uuid', |
| 83 | timestamp: 1621323232312, |
| 84 | event_message: 'some event happened', |
| 85 | metadata: { |
| 86 | my_key: 'something_value', |
| 87 | }, |
| 88 | }, |
| 89 | ]} |
| 90 | onRun={mockRun} |
| 91 | /> |
| 92 | ) |
| 93 | |
| 94 | const run = await screen.findByText('Run') |
| 95 | await userEvent.click(run) |
| 96 | // expect(mockRun).toBeCalled() |
| 97 | }) |
| 98 | |
| 99 | test('can run if no queryType provided', async () => { |
| 100 | const mockRun = vi.fn() |
| 101 | |
| 102 | render( |
| 103 | <LogTable |
| 104 | data={[ |
| 105 | { |
| 106 | id: 'some-uuid', |
| 107 | timestamp: 1621323232312, |
| 108 | event_message: 'some event happened', |
| 109 | metadata: { |
| 110 | my_key: 'something_value', |
| 111 | }, |
| 112 | }, |
| 113 | ]} |
| 114 | projectRef="abcd" |
| 115 | onRun={mockRun} |
| 116 | /> |
| 117 | ) |
| 118 | |
| 119 | const run = await screen.findByText('Run') |
| 120 | await userEvent.click(run) |
| 121 | // expect(mockRun).toBeCalled() |
| 122 | }) |
| 123 | |
| 124 | test('dedupes log lines with exact id', async () => { |
| 125 | // chronological mode requires 4 columns |
| 126 | render( |
| 127 | <LogTable |
| 128 | projectRef="projectRef" |
| 129 | data={[ |
| 130 | { |
| 131 | id: 'some-uuid', |
| 132 | timestamp: 1621323232312, |
| 133 | event_message: 'some event happened', |
| 134 | metadata: {}, |
| 135 | }, |
| 136 | { |
| 137 | id: 'some-uuid', |
| 138 | timestamp: 1621323232312, |
| 139 | event_message: 'some event happened', |
| 140 | metadata: {}, |
| 141 | }, |
| 142 | ]} |
| 143 | /> |
| 144 | ) |
| 145 | |
| 146 | // should only have one element, this line will fail if there are >1 element |
| 147 | await screen.findByText('timestamp') |
| 148 | }) |
| 149 | |
| 150 | test('can display standard preview table columns', async () => { |
| 151 | render( |
| 152 | <LogTable |
| 153 | projectRef="ref" |
| 154 | queryType="auth" |
| 155 | data={[{ id: '12345', event_message: 'some event message', timestamp: fakeMicroTimestamp }]} |
| 156 | /> |
| 157 | ) |
| 158 | await waitFor(() => screen.getByText(/some event message/)) |
| 159 | await expect(screen.findByText(/12345/)).rejects.toThrow() |
| 160 | await expect(screen.findByText(fakeMicroTimestamp)).rejects.toThrow() |
| 161 | }) |
| 162 | |
| 163 | test("closes the selection if the selected row's data changes", async () => { |
| 164 | const { rerender } = render( |
| 165 | <LogTable |
| 166 | projectRef="ref" |
| 167 | queryType="auth" |
| 168 | data={[{ id: '1', event_message: 'some event message', timestamp: fakeMicroTimestamp }]} |
| 169 | /> |
| 170 | ) |
| 171 | const text = await screen.findByText(/some event message/) |
| 172 | await userEvent.click(text) |
| 173 | |
| 174 | rerender( |
| 175 | <LogTable |
| 176 | projectRef="ref" |
| 177 | queryType="auth" |
| 178 | data={[{ id: '2', event_message: 'some other message', timestamp: fakeMicroTimestamp }]} |
| 179 | /> |
| 180 | ) |
| 181 | await expect(screen.findByText(/some event message/)).rejects.toThrow() |
| 182 | await screen.findByText(/some other message/) |
| 183 | }) |
| 184 | |
| 185 | enum QueryType { |
| 186 | Functions = 'functions', |
| 187 | Api = 'api', |
| 188 | Auth = 'auth', |
| 189 | } |
| 190 | test.each([ |
| 191 | { |
| 192 | queryType: QueryType.Functions, |
| 193 | data: [ |
| 194 | { |
| 195 | event_message: 'This is a error log\n', |
| 196 | event_type: 'log', |
| 197 | function_id: '001b0b08-331c-403e-810c-a2004b03a019', |
| 198 | level: 'error', |
| 199 | timestamp: 1659545029083869, |
| 200 | id: '3475cf6f-2929-4296-ab44-ce2c17069937', |
| 201 | }, |
| 202 | ], |
| 203 | includes: [/ERROR/], |
| 204 | excludes: ['undefined', 'null'], |
| 205 | }, |
| 206 | { |
| 207 | queryType: QueryType.Functions, |
| 208 | data: [ |
| 209 | { |
| 210 | event_message: 'This is a uncaughtExceptop\n', |
| 211 | event_type: 'uncaughtException', |
| 212 | function_id: '001b0b08-331c-403e-810c-a2004b03a019', |
| 213 | timestamp: 1659545029083869, |
| 214 | id: '4475cf6f-2929-4296-ab44-ce2c17069937', |
| 215 | level: undefined, |
| 216 | }, |
| 217 | ], |
| 218 | includes: [/uncaughtException/], |
| 219 | excludes: [/ERROR/], |
| 220 | }, |
| 221 | { |
| 222 | queryType: QueryType.Api, |
| 223 | data: [ |
| 224 | { |
| 225 | event_message: 'This is a uncaughtException\n', |
| 226 | path: 'this-is-some-path', |
| 227 | method: 'POST', |
| 228 | status_code: 500, |
| 229 | timestamp: 1659545029083869, |
| 230 | id: '4475cf6f-2929-4296-ab44-ce2c17069937', |
| 231 | }, |
| 232 | ], |
| 233 | includes: [/POST/, 'this-is-some-path'], |
| 234 | excludes: [], |
| 235 | }, |
| 236 | { |
| 237 | queryType: QueryType.Auth, |
| 238 | data: [ |
| 239 | { |
| 240 | event_message: JSON.stringify({ msg: 'some message', path: '/auth-path', level: 'info' }), |
| 241 | msg: 'some message', |
| 242 | path: '/auth-path', |
| 243 | level: 'info', |
| 244 | timestamp: 1659545029083869, |
| 245 | id: '4475cf6f-2929-4296-ab44-ce2c17069937', |
| 246 | }, |
| 247 | ], |
| 248 | includes: [/auth\-path/, /some message/, /INFO/], |
| 249 | excludes: [/\{/, /\}/], |
| 250 | }, |
| 251 | ])('table col renderer for $queryType', async ({ queryType, data, includes, excludes }) => { |
| 252 | render(<LogTable projectRef="ref" queryType={queryType} data={data} />) |
| 253 | |
| 254 | await Promise.all([ |
| 255 | ...includes.map((text) => screen.findByText(text)), |
| 256 | ...excludes.map((text) => expect(screen.findByText(text)).rejects.toThrow()), |
| 257 | ]) |
| 258 | }) |
| 259 | |
| 260 | test('error message handling', async () => { |
| 261 | // Render LogTable with error as a string |
| 262 | render(<LogTable projectRef="ref" error={'some error message'} />) |
| 263 | |
| 264 | expect(screen.getByText(`some error message`)).toBeTruthy() |
| 265 | |
| 266 | // Rerender LogTable with error as null |
| 267 | render(<LogTable projectRef="ref" error={null} />) |
| 268 | // Add any additional assertions if LogTable behaves differently when error is null |
| 269 | }) |
| 270 | |
| 271 | test('no results message handling', async () => { |
| 272 | render(<LogTable projectRef="ref" data={[]} />) |
| 273 | await screen.findByText(/No results/) |
| 274 | await screen.findByText(/Try another search/) |
| 275 | }) |
| 276 | |
| 277 | test('custom error message: Resources exceeded during query execution', async () => { |
| 278 | const errorFromLogflare = { |
| 279 | error: { |
| 280 | code: 400, |
| 281 | errors: [ |
| 282 | { |
| 283 | domain: 'global', |
| 284 | message: |
| 285 | 'Resources exceeded during query execution: The query could not be executed in the allotted memory. Peak usage: 122% of limit.\nTop memory consumer(s):\n ORDER BY operations: 99%\n other/unattributed: 1%\n', |
| 286 | reason: 'resourcesExceeded', |
| 287 | }, |
| 288 | ], |
| 289 | message: |
| 290 | 'Resources exceeded during query execution: The query could not be executed in the allotted memory. Peak usage: 122% of limit.\nTop memory consumer(s):\n ORDER BY operations: 99%\n other/unattributed: 1%\n', |
| 291 | status: 'INVALID_ARGUMENT', |
| 292 | }, |
| 293 | } |
| 294 | |
| 295 | // logs explorer, custom query |
| 296 | const { rerender } = render(<LogTable projectRef="ref" error={errorFromLogflare} />) |
| 297 | |
| 298 | // prompt user to reduce selected tables |
| 299 | await screen.findByText(/This query requires too much memory to be executed/) |
| 300 | await screen.findByText( |
| 301 | /Avoid selecting entire objects and instead select specific keys using dot notation/ |
| 302 | ) |
| 303 | |
| 304 | // previewer, prompt to reduce time range |
| 305 | rerender(<LogTable projectRef="ref" queryType="api" error={errorFromLogflare} />) |
| 306 | await screen.findByText(/This query requires too much memory to be executed/) |
| 307 | await screen.findByText(/Avoid querying across a large datetime range/) |
| 308 | await screen.findByText(/Please contact support if this error persists/) |
| 309 | }) |