addunit tests for session events parser (#3423)
* addunit tests for session events parser * fixed tests and add test check to deploy * updated frontend workflow * updated frontend workflow * updated frontend workflow * updated frontend workflow * updated frontend workflow * updated frontend workflow * fix test
This commit is contained in:
parent
dedeb4cb2c
commit
d3d1a40909
21 changed files with 1358 additions and 124 deletions
33
.github/workflows/frontend-tests.yaml
vendored
Normal file
33
.github/workflows/frontend-tests.yaml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Frontend tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '.github/workflows/frontend-test.yaml'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: yarn
|
||||
|
||||
- name: Run tests
|
||||
working-directory: frontend
|
||||
run: yarn test:ci
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
directory: frontend/coverage/
|
||||
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { TextDecoder } from 'util';
|
||||
|
||||
export default class PrimitiveReader {
|
||||
/** pointer for curent position in the buffer */
|
||||
protected p: number = 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { DateTime, Interval, Settings } from 'luxon';
|
||||
import Record from 'Types/Record';
|
||||
import { roundToNextMinutes } from '@/utils';
|
||||
|
||||
export const LAST_30_MINUTES = 'LAST_30_MINUTES';
|
||||
export const TODAY = 'TODAY';
|
||||
|
|
|
|||
|
|
@ -69,74 +69,77 @@ interface IosCrash {
|
|||
|
||||
export interface ISession {
|
||||
sessionId: string;
|
||||
pageTitle: string;
|
||||
active: boolean;
|
||||
siteId: string;
|
||||
projectKey: string;
|
||||
peerId: string;
|
||||
live: boolean;
|
||||
startedAt: number;
|
||||
pageTitle?: string;
|
||||
active?: boolean;
|
||||
siteId?: string;
|
||||
projectKey?: string;
|
||||
peerId?: string;
|
||||
live?: boolean;
|
||||
startedAt?: number;
|
||||
duration: number;
|
||||
durationMs: number;
|
||||
events: InjectedEvent[];
|
||||
crashes: IosCrash[];
|
||||
stackEvents: StackEvent[];
|
||||
metadata: [];
|
||||
favorite: boolean;
|
||||
durationMs?: number;
|
||||
events?: InjectedEvent[];
|
||||
crashes?: IosCrash[];
|
||||
stackEvents?: StackEvent[];
|
||||
metadata: {
|
||||
userId?: string | null;
|
||||
demo?: string | null;
|
||||
};
|
||||
favorite?: boolean;
|
||||
filterId?: string;
|
||||
canvasURL: string[];
|
||||
domURL: string[];
|
||||
devtoolsURL: string[];
|
||||
uxtVideo: string[];
|
||||
canvasURL?: string[];
|
||||
domURL?: string[];
|
||||
devtoolsURL?: string[];
|
||||
uxtVideo?: string[];
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
mobsUrl: string[];
|
||||
mobsUrl?: string[];
|
||||
userBrowser: string;
|
||||
userBrowserVersion: string;
|
||||
userBrowserVersion?: string;
|
||||
userCountry: string;
|
||||
userCity: string;
|
||||
userState: string;
|
||||
userDevice: string;
|
||||
userDevice?: string | null;
|
||||
userDeviceType: string;
|
||||
isMobile: boolean;
|
||||
isMobile?: boolean;
|
||||
userOs: string;
|
||||
userOsVersion: string;
|
||||
userOsVersion?: string;
|
||||
userId: string;
|
||||
userAnonymousId: string;
|
||||
userAnonymousId?: string | null;
|
||||
userUuid: string;
|
||||
userDisplayName: string;
|
||||
userNumericHash: number;
|
||||
userDisplayName?: string;
|
||||
userNumericHash?: number;
|
||||
viewed: boolean;
|
||||
consoleLogCount: number;
|
||||
consoleLogCount?: number;
|
||||
eventsCount: number;
|
||||
pagesCount: number;
|
||||
errorsCount: number;
|
||||
issueTypes: string[];
|
||||
issues: IIssue[];
|
||||
referrer: string | null;
|
||||
userDeviceHeapSize: number;
|
||||
userDeviceMemorySize: number;
|
||||
errors: SessionError[];
|
||||
socket: string;
|
||||
isIOS: boolean;
|
||||
revId: string | null;
|
||||
issues?: IIssue[];
|
||||
referrer?: string | null;
|
||||
userDeviceHeapSize?: number;
|
||||
userDeviceMemorySize?: number;
|
||||
errors?: SessionError[];
|
||||
socket?: string;
|
||||
isIOS?: boolean;
|
||||
revId?: string | null;
|
||||
agentIds?: string[];
|
||||
isCallActive?: boolean;
|
||||
agentToken: string;
|
||||
notes: Note[];
|
||||
notesWithEvents: Array<Note | InjectedEvent>;
|
||||
fileKey: string;
|
||||
agentToken?: string;
|
||||
notes?: Note[];
|
||||
notesWithEvents?: Array<Note | InjectedEvent>;
|
||||
fileKey?: string;
|
||||
platform: 'web' | 'ios' | 'android';
|
||||
projectId: string;
|
||||
projectId: number | string;
|
||||
startTs: number;
|
||||
timestamp: number;
|
||||
backendErrors: number;
|
||||
consoleErrors: number;
|
||||
timestamp?: number;
|
||||
backendErrors?: number;
|
||||
consoleErrors?: number;
|
||||
sessionID?: string;
|
||||
userID: string;
|
||||
userUUID: string;
|
||||
userEvents: any[];
|
||||
userID?: string;
|
||||
userUUID?: string;
|
||||
userEvents?: any[];
|
||||
timezone?: string;
|
||||
videoURL?: string[];
|
||||
isMobileNative?: boolean;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ export default {
|
|||
'^Types/(.+)$': '<rootDir>/app/types/$1',
|
||||
'^App/(.+)$': '<rootDir>/app/$1',
|
||||
"\\.(css|less)$": "<rootDir>/tests/mocks/style.mock.js",
|
||||
'^@/(.*)$': '<rootDir>/app/$1',
|
||||
'^Player/(.+)$': '<rootDir>/app/player/$1',
|
||||
'^Player$': '<rootDir>/app/player',
|
||||
'^UI/(.+)$': '<rootDir>/app/components/ui/$1',
|
||||
'^UI$': '<rootDir>/app/components/ui',
|
||||
'^Shared/(.+)$': '<rootDir>/app/components/shared/$1',
|
||||
'\\.svg$': '<rootDir>/tests/mocks/svgMock.js',
|
||||
'^Components/(.+)$': '<rootDir>/app/components/$1',
|
||||
},
|
||||
collectCoverage: true,
|
||||
verbose: true,
|
||||
|
|
@ -23,4 +31,5 @@ export default {
|
|||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!syncod)',
|
||||
],
|
||||
setupFiles: ['<rootDir>/tests/jest.setup.ts'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
"@openreplay/sourcemap-uploader": "^3.0.10",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/eslint-plugin-jsx-a11y": "^6",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.7.8",
|
||||
"@types/prismjs": "^1",
|
||||
|
|
@ -136,7 +137,7 @@
|
|||
"eslint-plugin-react": "^7.29.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"jest": "^29.5.0",
|
||||
"jest": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"minio": "^7.1.3",
|
||||
"node-gyp": "^9.0.0",
|
||||
|
|
@ -154,7 +155,7 @@
|
|||
"svgo": "^2.8.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"thread-loader": "^4.0.4",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-jest": "^29.3.3",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
|
|
|
|||
83
frontend/tests/ListWalker.test.ts
Normal file
83
frontend/tests/ListWalker.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { describe, test, expect, beforeEach, jest } from '@jest/globals';
|
||||
import ListWalker from '../app/player/common/ListWalker';
|
||||
import type { Timed } from '../app/player/common/types';
|
||||
|
||||
interface Item extends Timed {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
describe('ListWalker', () => {
|
||||
let walker: ListWalker<Item>;
|
||||
|
||||
beforeEach(() => {
|
||||
walker = new ListWalker<Item>([]);
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
test('append maintains order and prevents out of order inserts', () => {
|
||||
walker.append({ time: 1 });
|
||||
walker.append({ time: 3 });
|
||||
expect(walker.list.map(i => i.time)).toEqual([1, 3]);
|
||||
|
||||
walker.append({ time: 2 });
|
||||
expect(walker.list.map(i => i.time)).toEqual([1, 3]);
|
||||
expect((console.error as jest.Mock).mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('unshift prepends items', () => {
|
||||
walker.append({ time: 2 });
|
||||
walker.unshift({ time: 1 });
|
||||
expect(walker.list.map(i => i.time)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('insert places item according to time', () => {
|
||||
walker.append({ time: 1 });
|
||||
walker.append({ time: 3 });
|
||||
walker.insert({ time: 2 });
|
||||
expect(walker.list.map(i => i.time)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('moveGetLast advances pointer and returns item', () => {
|
||||
walker = new ListWalker<Item>([{ time: 1 }, { time: 3 }, { time: 5 }]);
|
||||
|
||||
expect(walker.moveGetLast(3)?.time).toBe(3);
|
||||
expect(walker.countNow).toBe(2);
|
||||
|
||||
expect(walker.moveGetLast(3)).toBeNull();
|
||||
expect(walker.moveGetLast(4)).toBeNull();
|
||||
expect(walker.moveGetLast(4, undefined, true)?.time).toBe(3);
|
||||
|
||||
expect(walker.moveGetLast(1)?.time).toBe(1);
|
||||
expect(walker.countNow).toBe(1);
|
||||
});
|
||||
|
||||
test('getNew returns items when pointer moves or time decreases', () => {
|
||||
walker = new ListWalker<Item>([{ time: 1 }, { time: 3 }, { time: 5 }]);
|
||||
|
||||
expect(walker.getNew(2)?.time).toBe(1);
|
||||
expect(walker.getNew(4)?.time).toBe(3);
|
||||
expect(walker.getNew(4)).toBeNull();
|
||||
expect(walker.getNew(1)?.time).toBe(1);
|
||||
});
|
||||
|
||||
test('findLast performs binary search', () => {
|
||||
walker = new ListWalker<Item>([{ time: 1 }, { time: 3 }, { time: 5 }]);
|
||||
|
||||
expect(walker.findLast(4)?.time).toBe(3);
|
||||
expect(walker.findLast(5)?.time).toBe(5);
|
||||
expect(walker.findLast(0)).toBeNull();
|
||||
});
|
||||
|
||||
test('moveApply iterates over items up to time', () => {
|
||||
walker = new ListWalker<Item>([{ time: 1 }, { time: 2 }, { time: 3 }]);
|
||||
const collected: number[] = [];
|
||||
|
||||
walker.moveApply(2.5, (m) => collected.push(m.time));
|
||||
expect(collected).toEqual([1, 2]);
|
||||
expect(walker.countNow).toBe(2);
|
||||
|
||||
walker.moveApply(1.5, (m) => collected.push(m.time));
|
||||
expect(collected).toEqual([1, 2, 1]);
|
||||
expect(walker.countNow).toBe(1);
|
||||
});
|
||||
});
|
||||
47
frontend/tests/MFileReader.test.ts
Normal file
47
frontend/tests/MFileReader.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, test, expect } from '@jest/globals';
|
||||
import MFileReader from '../app/player/web/messages/MFileReader';
|
||||
import { MType } from '../app/player/web/messages/raw.gen';
|
||||
|
||||
function encodeUint(value: number): Uint8Array {
|
||||
const bytes: number[] = [];
|
||||
let v = value;
|
||||
do {
|
||||
let byte = v & 0x7f;
|
||||
v >>>= 7;
|
||||
if (v) byte |= 0x80;
|
||||
bytes.push(byte);
|
||||
} while (v);
|
||||
return Uint8Array.from(bytes);
|
||||
}
|
||||
|
||||
function concat(...parts: Uint8Array[]): Uint8Array {
|
||||
const total = parts.reduce((s, p) => s + p.length, 0);
|
||||
const out = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const p of parts) {
|
||||
out.set(p, offset);
|
||||
offset += p.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe('MFileReader', () => {
|
||||
test('checkForIndexes detects missing indexes and skips header', () => {
|
||||
const data = new Uint8Array(9).fill(0xff);
|
||||
const reader = new MFileReader(data);
|
||||
reader.checkForIndexes();
|
||||
expect(reader['noIndexes']).toBe(true);
|
||||
expect(reader['p']).toBe(8);
|
||||
reader.checkForIndexes();
|
||||
expect(reader['p']).toBe(8);
|
||||
});
|
||||
|
||||
test('readNext returns timestamp message and sets startTime', () => {
|
||||
const data = concat(encodeUint(MType.Timestamp), encodeUint(2000));
|
||||
const reader = new MFileReader(data);
|
||||
reader['noIndexes'] = true;
|
||||
const msg = reader.readNext();
|
||||
expect(msg).toEqual({ tp: 9999, tabId: '', time: 0 });
|
||||
expect(reader['startTime']).toBe(2000);
|
||||
});
|
||||
});
|
||||
205
frontend/tests/MessageLoader.test.ts
Normal file
205
frontend/tests/MessageLoader.test.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
|
||||
import MessageLoader from '../app/player/web/MessageLoader';
|
||||
import { MType } from '../app/player/web/messages';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { TextDecoder } from 'util';
|
||||
|
||||
const loadFilesMock = jest.fn(async () => {});
|
||||
|
||||
jest.mock('../app/player/web/network/loadFiles', () => ({
|
||||
__esModule: true,
|
||||
loadFiles: jest.fn(async () => {}),
|
||||
requestTarball: jest.fn(),
|
||||
requestEFSDom: jest.fn(),
|
||||
requestEFSDevtools: jest.fn(),
|
||||
}));
|
||||
|
||||
const decryptSessionBytesMock = jest.fn((b: Uint8Array) => Promise.resolve(b));
|
||||
|
||||
jest.mock('../app/player/web/network/crypto', () => ({
|
||||
__esModule: true,
|
||||
decryptSessionBytes: jest.fn((b: Uint8Array) => Promise.resolve(b)),
|
||||
}));
|
||||
|
||||
jest.mock('Player/common/unpack', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((b: Uint8Array) => b),
|
||||
}));
|
||||
|
||||
jest.mock('Player/common/tarball', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((b: Uint8Array) => b),
|
||||
}));
|
||||
|
||||
import MFileReader from '../app/player/web/messages/MFileReader';
|
||||
|
||||
const readNextMock = jest.fn();
|
||||
|
||||
jest.mock('../app/player/web/messages/MFileReader', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
append: jest.fn(),
|
||||
checkForIndexes: jest.fn(),
|
||||
readNext: readNextMock,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import { mockSession } from './mocks/sessionData';
|
||||
|
||||
const createStore = () => {
|
||||
const state: Record<string, any> = {};
|
||||
return {
|
||||
get: () => state,
|
||||
update: jest.fn((s: any) => Object.assign(state, s)),
|
||||
updateTabStates: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createManager = () => ({
|
||||
distributeMessage: jest.fn(),
|
||||
sortDomRemoveMessages: jest.fn(),
|
||||
setMessagesLoading: jest.fn(),
|
||||
startLoading: jest.fn(),
|
||||
getListsFullState: jest.fn(() => ({ list: true })),
|
||||
createTabCloseEvents: jest.fn(),
|
||||
onFileReadFinally: jest.fn(),
|
||||
onFileReadSuccess: jest.fn(),
|
||||
onFileReadFailed: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('MessageLoader.loadDomFiles', () => {
|
||||
test('loads dom files and updates store', async () => {
|
||||
const session = mockSession({});
|
||||
const store = createStore();
|
||||
const loader = new MessageLoader(
|
||||
session,
|
||||
store as any,
|
||||
createManager() as any,
|
||||
false,
|
||||
);
|
||||
|
||||
const parser = jest.fn();
|
||||
await loader.loadDomFiles(['u1', 'u2'], parser);
|
||||
|
||||
expect(store.update).toHaveBeenNthCalledWith(1, { domLoading: true });
|
||||
expect(store.update).toHaveBeenNthCalledWith(2, { domLoading: false });
|
||||
});
|
||||
|
||||
test('skips when no urls provided', async () => {
|
||||
const loader = new MessageLoader(
|
||||
mockSession({}),
|
||||
createStore() as any,
|
||||
createManager() as any,
|
||||
false,
|
||||
);
|
||||
const parser = jest.fn();
|
||||
await loader.loadDomFiles([], parser);
|
||||
expect(loadFilesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MessageLoader.loadDevtools', () => {
|
||||
test('loads devtools when not clickmap', async () => {
|
||||
const session = mockSession({});
|
||||
session.devtoolsURL = ['d1'];
|
||||
const store = createStore();
|
||||
const manager = createManager();
|
||||
const loader = new MessageLoader(
|
||||
session,
|
||||
store as any,
|
||||
manager as any,
|
||||
false,
|
||||
);
|
||||
|
||||
const parser = jest.fn();
|
||||
await loader.loadDevtools(parser);
|
||||
|
||||
expect(store.update).toHaveBeenCalledWith({ devtoolsLoading: true });
|
||||
expect(store.update).toHaveBeenLastCalledWith({
|
||||
...manager.getListsFullState(),
|
||||
devtoolsLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('skips devtools for clickmap', async () => {
|
||||
const session = mockSession({});
|
||||
session.devtoolsURL = ['d1'];
|
||||
const loader = new MessageLoader(
|
||||
session,
|
||||
createStore() as any,
|
||||
createManager() as any,
|
||||
true,
|
||||
);
|
||||
await loader.loadDevtools(jest.fn());
|
||||
expect(loadFilesMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MessageLoader.createTabCloseEvents', () => {
|
||||
test('delegates to manager when method exists', () => {
|
||||
const manager = createManager();
|
||||
const loader = new MessageLoader(
|
||||
mockSession({}),
|
||||
createStore() as any,
|
||||
manager as any,
|
||||
false,
|
||||
);
|
||||
loader.createTabCloseEvents();
|
||||
expect(manager.createTabCloseEvents).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MessageLoader.preloadFirstFile', () => {
|
||||
test('stores key and marks as preloaded', async () => {
|
||||
const loader = new MessageLoader(
|
||||
mockSession({}),
|
||||
createStore() as any,
|
||||
createManager() as any,
|
||||
false,
|
||||
);
|
||||
const parser = jest.fn(() => Promise.resolve());
|
||||
jest.spyOn(loader, 'createNewParser').mockReturnValue(parser);
|
||||
|
||||
await loader.preloadFirstFile(new Uint8Array([1]), 'key');
|
||||
|
||||
expect(loader.session.fileKey).toBe('key');
|
||||
expect(parser).toHaveBeenCalled();
|
||||
expect(loader.preloaded).toBe(true);
|
||||
expect(loader.mobParser).toBe(parser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MessageLoader.createNewParser', () => {
|
||||
test('parses messages and sorts them', async () => {
|
||||
const loader = new MessageLoader(
|
||||
mockSession({}),
|
||||
createStore() as any,
|
||||
createManager() as any,
|
||||
false,
|
||||
);
|
||||
const msgs = [
|
||||
{ tp: MType.SetNodeAttribute, time: 2 },
|
||||
{ tp: MType.SetNodeAttribute, time: 1 },
|
||||
];
|
||||
readNextMock
|
||||
.mockReturnValueOnce(msgs[0])
|
||||
.mockReturnValueOnce(msgs[1])
|
||||
.mockReturnValueOnce(null);
|
||||
|
||||
const onDone = jest.fn();
|
||||
const parser = loader.createNewParser(false, onDone, 'file');
|
||||
await parser(new Uint8Array());
|
||||
|
||||
expect(onDone).toHaveBeenCalledWith([msgs[1], msgs[0]], 'file 1');
|
||||
expect(loader.rawMessages.length).toBe(2);
|
||||
});
|
||||
});
|
||||
101
frontend/tests/TabManager.test.ts
Normal file
101
frontend/tests/TabManager.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import TabSessionManager from '../app/player/web/TabManager';
|
||||
import SimpleStore from '../app/player/common/SimpleStore';
|
||||
import { TYPES as EVENT_TYPES } from '../app/types/session/event';
|
||||
import { MType } from '../app/player/web/messages/raw.gen';
|
||||
|
||||
jest.mock('@medv/finder', () => ({ default: jest.fn(() => 'mocked network-proxy content') }));
|
||||
jest.mock('syncod', () => {
|
||||
return {
|
||||
Decoder: jest.fn().mockImplementation(() => ({ decode: jest.fn(), set: jest.fn() })),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('js-untar', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
class FakeScreen {
|
||||
displayFrame = jest.fn();
|
||||
window: any = null;
|
||||
document: any = { body: { querySelector: jest.fn(), style: {} } };
|
||||
}
|
||||
|
||||
const session = { isMobile: false } as any;
|
||||
const setSize = jest.fn();
|
||||
|
||||
let store: SimpleStore<any>;
|
||||
let manager: TabSessionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
store = new SimpleStore({
|
||||
tabStates: { tab1: { ...TabSessionManager.INITIAL_STATE } },
|
||||
tabNames: {},
|
||||
eventCount: 0,
|
||||
});
|
||||
manager = new TabSessionManager(session, store as any, new FakeScreen() as any, 'tab1', setSize, 0);
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('updateLists should append location events and update store', () => {
|
||||
const event = { time: 1, key: 1, type: EVENT_TYPES.LOCATION } as any;
|
||||
manager.updateLists({ event: [event] });
|
||||
// @ts-ignore private access
|
||||
expect((manager as any).locationEventManager.list[0]).toBe(event);
|
||||
expect(store.get().tabStates['tab1'].eventList).toEqual([event]);
|
||||
expect(store.get().eventCount).toBe(1);
|
||||
});
|
||||
|
||||
it('resetMessageManagers should clear managers', () => {
|
||||
// @ts-ignore private access
|
||||
(manager as any).locationEventManager.append({ time: 1 });
|
||||
// @ts-ignore private access
|
||||
(manager as any).scrollManager.append({ time: 2 });
|
||||
const oldPages = (manager as any).pagesManager;
|
||||
manager.resetMessageManagers();
|
||||
// @ts-ignore private access
|
||||
expect((manager as any).locationEventManager.list.length).toBe(0);
|
||||
// @ts-ignore private access
|
||||
expect((manager as any).scrollManager.list.length).toBe(0);
|
||||
// @ts-ignore private access
|
||||
expect((manager as any).pagesManager).not.toBe(oldPages);
|
||||
});
|
||||
|
||||
it('onFileReadSuccess should update store with lists and performance data', () => {
|
||||
(manager as any).performanceTrackManager['chart'] = [{ time: 1, usedHeap: 0, totalHeap: 0, fps: null, cpu: null, nodesCount: 0 }];
|
||||
(manager as any).performanceTrackManager['cpuAvailable'] = true;
|
||||
(manager as any).performanceTrackManager['fpsAvailable'] = true;
|
||||
manager.locationManager.append({ time: 2, url: 'http://example.com' } as any);
|
||||
manager.onFileReadSuccess();
|
||||
const state = store.get().tabStates['tab1'];
|
||||
expect(state.performanceChartData.length).toBe(1);
|
||||
expect(state.performanceAvailability).toEqual({ cpu: true, fps: true, heap: false, nodes: true });
|
||||
expect(state.urlsList[0].url).toBe('http://example.com');
|
||||
});
|
||||
|
||||
it('decodeMessage should delegate to decoder', () => {
|
||||
const msg = { tp: MType.Timestamp, time: 0 } as any;
|
||||
const decoder = (manager as any).decoder;
|
||||
manager.decodeMessage(msg);
|
||||
expect(decoder.decode).toHaveBeenCalledWith(msg);
|
||||
});
|
||||
|
||||
it('sortDomRemoveMessages comparator should prioritize head nodes', () => {
|
||||
const mock = { sortPages: jest.fn() };
|
||||
// @ts-ignore private access
|
||||
(manager as any).pagesManager = mock;
|
||||
const msgs = [
|
||||
{ id: 1, parentID: 1, tp: MType.RemoveNode, time: 10 },
|
||||
{ id: 2, parentID: 2, tp: MType.RemoveNode, time: 10 },
|
||||
{ id: 3, parentID: 2, tp: MType.CreateElementNode, time: 10 },
|
||||
] as any[];
|
||||
manager.sortDomRemoveMessages(msgs);
|
||||
const comparator = mock.sortPages.mock.calls[0][0];
|
||||
expect(comparator(msgs[0], msgs[2])).toBe(-1);
|
||||
expect(comparator(msgs[2], msgs[0])).toBe(1);
|
||||
expect(comparator(msgs[0], msgs[1])).toBe(-1);
|
||||
expect(comparator(msgs[1], msgs[0])).toBe(1);
|
||||
});
|
||||
144
frontend/tests/create.test.ts
Normal file
144
frontend/tests/create.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
|
||||
|
||||
class MockIOSPlayer {
|
||||
static INITIAL_STATE = { ios: true };
|
||||
public toggleRange = jest.fn();
|
||||
constructor(
|
||||
public store: any,
|
||||
public session: any,
|
||||
public handler?: any,
|
||||
) {}
|
||||
}
|
||||
class MockWebPlayer {
|
||||
static INITIAL_STATE = { web: true };
|
||||
public toggleRange = jest.fn();
|
||||
constructor(
|
||||
public store: any,
|
||||
public session: any,
|
||||
public live: boolean,
|
||||
public clickMap: boolean,
|
||||
public handler?: any,
|
||||
public prefetched?: boolean,
|
||||
) {}
|
||||
}
|
||||
class MockWebLivePlayer {
|
||||
static INITIAL_STATE = { live: true };
|
||||
constructor(
|
||||
public store: any,
|
||||
public session: any,
|
||||
public config: any,
|
||||
public agentId: number,
|
||||
public projectId: number,
|
||||
public handler?: any,
|
||||
) {}
|
||||
}
|
||||
|
||||
jest.mock('../app/player/mobile/IOSPlayer', () => ({
|
||||
__esModule: true,
|
||||
default: MockIOSPlayer,
|
||||
}));
|
||||
|
||||
jest.mock('../app/player/web/WebPlayer', () => ({
|
||||
__esModule: true,
|
||||
default: MockWebPlayer,
|
||||
}));
|
||||
|
||||
jest.mock('../app/player/web/WebLivePlayer', () => ({
|
||||
__esModule: true,
|
||||
default: MockWebLivePlayer,
|
||||
}));
|
||||
|
||||
import {
|
||||
createIOSPlayer,
|
||||
createWebPlayer,
|
||||
createClickMapPlayer,
|
||||
createLiveWebPlayer,
|
||||
createClipPlayer,
|
||||
} from '../app/player/create';
|
||||
|
||||
const session = { id: 1 } as any;
|
||||
const errorHandler = { error: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('player factory functions', () => {
|
||||
it('createIOSPlayer returns player and store', () => {
|
||||
const [player, store] = createIOSPlayer(session, undefined, errorHandler);
|
||||
expect(player).toBeInstanceOf(MockIOSPlayer);
|
||||
expect(store.get()).toEqual(MockIOSPlayer.INITIAL_STATE);
|
||||
expect((player as any).store).toBe(store);
|
||||
expect((player as any).session).toBe(session);
|
||||
expect((player as any).handler).toBe(errorHandler);
|
||||
});
|
||||
|
||||
it('createIOSPlayer applies wrapStore', () => {
|
||||
const wrapper = jest.fn((s) => s);
|
||||
const [, store] = createIOSPlayer(session, wrapper);
|
||||
expect(wrapper).toHaveBeenCalledWith(store);
|
||||
});
|
||||
|
||||
it('createWebPlayer passes arguments correctly', () => {
|
||||
const [player, store] = createWebPlayer(
|
||||
session,
|
||||
undefined,
|
||||
errorHandler,
|
||||
true,
|
||||
);
|
||||
expect(player).toBeInstanceOf(MockWebPlayer);
|
||||
expect(store.get()).toEqual(MockWebPlayer.INITIAL_STATE);
|
||||
expect((player as any).live).toBe(false);
|
||||
expect((player as any).clickMap).toBe(false);
|
||||
expect((player as any).prefetched).toBe(true);
|
||||
});
|
||||
|
||||
it('createClickMapPlayer creates click map WebPlayer', () => {
|
||||
const [player] = createClickMapPlayer(session);
|
||||
expect(player).toBeInstanceOf(MockWebPlayer);
|
||||
expect((player as any).clickMap).toBe(true);
|
||||
});
|
||||
|
||||
it('createLiveWebPlayer passes all params', () => {
|
||||
const cfg = [{ url: 'stun:test' }] as any;
|
||||
const [player, store] = createLiveWebPlayer(
|
||||
session,
|
||||
cfg,
|
||||
5,
|
||||
7,
|
||||
undefined,
|
||||
errorHandler,
|
||||
);
|
||||
expect(player).toBeInstanceOf(MockWebLivePlayer);
|
||||
expect(store.get()).toEqual(MockWebLivePlayer.INITIAL_STATE);
|
||||
expect((player as any).config).toBe(cfg);
|
||||
expect((player as any).agentId).toBe(5);
|
||||
expect((player as any).projectId).toBe(7);
|
||||
});
|
||||
|
||||
it('createClipPlayer mobile uses IOSPlayer and toggles range', () => {
|
||||
const range: [number, number] = [1, 5];
|
||||
const [player] = createClipPlayer(
|
||||
session,
|
||||
undefined,
|
||||
undefined,
|
||||
range,
|
||||
true,
|
||||
);
|
||||
expect(player).toBeInstanceOf(MockIOSPlayer);
|
||||
expect((player as any).toggleRange).toHaveBeenCalledWith(1, 5);
|
||||
});
|
||||
|
||||
it('createClipPlayer web uses WebPlayer when not mobile', () => {
|
||||
const range: [number, number] = [1, 5];
|
||||
const [player] = createClipPlayer(
|
||||
session,
|
||||
undefined,
|
||||
undefined,
|
||||
range,
|
||||
false,
|
||||
);
|
||||
expect(player).toBeInstanceOf(MockWebPlayer);
|
||||
expect((player as any).toggleRange).toHaveBeenCalledWith(1, 5);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, jest, test } from '@jest/globals';
|
||||
|
||||
import FeatureFlagsStore from 'App/mstore/FeatureFlagsStore';
|
||||
import FeatureFlagsStore from 'App/mstore/featureFlagsStore';
|
||||
import FeatureFlag from 'App/mstore/types/FeatureFlag';
|
||||
|
||||
const mockFlags = [{ featureFlagId: 1 }, { featureFlagId: 2 }];
|
||||
|
|
|
|||
26
frontend/tests/jest.setup.ts
Normal file
26
frontend/tests/jest.setup.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
||||
value: jest.fn(() => ({
|
||||
canvas: {
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
fillRect: jest.fn(),
|
||||
clearRect: jest.fn(),
|
||||
getImageData: jest.fn(() => ({ data: [] })),
|
||||
putImageData: jest.fn(),
|
||||
createImageData: jest.fn(() => []),
|
||||
setTransform: jest.fn(),
|
||||
drawImage: jest.fn(),
|
||||
save: jest.fn(),
|
||||
fillText: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
beginPath: jest.fn(),
|
||||
moveTo: jest.fn(),
|
||||
lineTo: jest.fn(),
|
||||
closePath: jest.fn(),
|
||||
stroke: jest.fn(),
|
||||
strokeRect: jest.fn(),
|
||||
arc: jest.fn(),
|
||||
fill: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { ISession, } from '@/types/session/session';
|
||||
|
||||
export const issues = [
|
||||
{
|
||||
issueId: '9158adad14bcb1e0db18384c778a18f5ef2',
|
||||
|
|
@ -84,7 +86,8 @@ export const events = [
|
|||
},
|
||||
{
|
||||
time: 13146,
|
||||
label: 'Search sessions using any captured event (click, input, page, error...)',
|
||||
label:
|
||||
'Search sessions using any captured event (click, input, page, error...)',
|
||||
key: 3,
|
||||
target: { path: undefined, label: undefined },
|
||||
type: 'CLICK',
|
||||
|
|
@ -145,7 +148,8 @@ export const events = [
|
|||
},
|
||||
{
|
||||
time: 15166,
|
||||
label: 'Search sessions using any captured event (click, input, page, error...)',
|
||||
label:
|
||||
'Search sessions using any captured event (click, input, page, error...)',
|
||||
key: 7,
|
||||
target: { path: undefined, label: undefined },
|
||||
type: 'CLICK',
|
||||
|
|
@ -155,7 +159,8 @@ export const events = [
|
|||
},
|
||||
{
|
||||
time: 15726,
|
||||
label: 'Search sessions using any captured event (click, input, page, error...)',
|
||||
label:
|
||||
'Search sessions using any captured event (click, input, page, error...)',
|
||||
key: 8,
|
||||
target: { path: undefined, label: undefined },
|
||||
type: 'INPUT',
|
||||
|
|
@ -173,3 +178,32 @@ export const events = [
|
|||
count: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockSession: (props: { duration?: number, projectId?: string, sessionId?: string, favorite?: boolean }) => ISession = ({
|
||||
duration = 34490, projectId = '5095', sessionId = '3315944703327552482', favorite = false,
|
||||
}) => ({
|
||||
duration,
|
||||
errorsCount: 0,
|
||||
eventsCount: 0,
|
||||
issueScore: 1747645706,
|
||||
issueTypes: [],
|
||||
metadata: {userId: null, demo: null},
|
||||
pagesCount: 0,
|
||||
platform: 'web',
|
||||
projectId,
|
||||
sessionId,
|
||||
startTs: 1747829271755,
|
||||
timezone: 'UTC+08:00',
|
||||
userAnonymousId: 'null',
|
||||
userBrowser: 'Chrome',
|
||||
userCity: 'Singapore',
|
||||
userCountry: 'SG',
|
||||
userDevice: 'null',
|
||||
userDeviceType: 'desktop',
|
||||
userId: '',
|
||||
userOs: 'Mac OS X',
|
||||
userState: '',
|
||||
userUuid: 'b8268c1d-9c5d-4b47-9893-5c0a040adc8f',
|
||||
viewed: false,
|
||||
favorite,
|
||||
});
|
||||
4
frontend/tests/mocks/svgMock.js
Normal file
4
frontend/tests/mocks/svgMock.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
__esModule: true,
|
||||
default: 'SvgMock',
|
||||
};
|
||||
139
frontend/tests/searchStore.test.ts
Normal file
139
frontend/tests/searchStore.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { describe, expect, it, beforeEach, jest } from '@jest/globals';
|
||||
|
||||
jest.mock('App/mstore/types/filterItem', () => {
|
||||
return class FilterItem {
|
||||
key: any;
|
||||
value: any;
|
||||
operator: any;
|
||||
source: any;
|
||||
sourceOperator: any;
|
||||
isEvent: any;
|
||||
filters: any;
|
||||
constructor(data: any = {}) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
fromJson(data: any) {
|
||||
Object.assign(this, data);
|
||||
return this;
|
||||
}
|
||||
merge(data: any) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
type: this.key || this.type,
|
||||
value: this.value,
|
||||
operator: this.operator,
|
||||
source: this.source,
|
||||
sourceOperator: this.sourceOperator,
|
||||
isEvent: this.isEvent,
|
||||
filters: Array.isArray(this.filters) ? this.filters : [],
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('Types/filter/newFilter', () => {
|
||||
const { FilterKey, FilterCategory } = require('Types/filter/filterType');
|
||||
return {
|
||||
filtersMap: {
|
||||
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterKey.USERID, category: FilterCategory.USER, operator: 'is', value: [''] },
|
||||
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterKey.DURATION, category: FilterCategory.SESSION, operator: 'is', value: [0, 0] },
|
||||
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterKey.ISSUE, category: FilterCategory.ISSUE, operator: 'is', value: [] },
|
||||
},
|
||||
conditionalFiltersMap: {},
|
||||
generateFilterOptions: jest.fn(() => []),
|
||||
liveFiltersMap: {},
|
||||
mobileConditionalFiltersMap: {},
|
||||
};
|
||||
});
|
||||
|
||||
const mockSessionFetch = jest.fn().mockResolvedValue({});
|
||||
|
||||
const mockSessionStore = {
|
||||
fetchSessions: mockSessionFetch,
|
||||
total: 0,
|
||||
clearList: jest.fn(),
|
||||
};
|
||||
const mockSettingsStore = {
|
||||
sessionSettings: { durationFilter: { count: 0 } },
|
||||
};
|
||||
|
||||
jest.mock('App/services', () => ({
|
||||
searchService: { fetchSavedSearch: jest.fn() },
|
||||
sessionService: { getSessions: jest.fn().mockResolvedValue({ sessions: [], total: 0 }) },
|
||||
}));
|
||||
jest.mock('App/mstore', () => ({
|
||||
sessionStore: mockSessionStore,
|
||||
settingsStore: mockSettingsStore,
|
||||
}));
|
||||
|
||||
import SearchStore, { checkValues, filterMap } from '../app/mstore/searchStore';
|
||||
import SavedSearch from '../app/mstore/types/savedSearch';
|
||||
import { FilterCategory, FilterKey } from '../app/types/filter/filterType';
|
||||
|
||||
|
||||
describe('searchStore utilities', () => {
|
||||
it('checkValues handles duration', () => {
|
||||
const res = checkValues(FilterKey.DURATION, ['', 1000]);
|
||||
expect(res).toEqual([0, 1000]);
|
||||
});
|
||||
|
||||
it('checkValues filters empty values', () => {
|
||||
const res = checkValues(FilterKey.USERID, ['a', '', null]);
|
||||
expect(res).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('filterMap maps metadata type correctly', () => {
|
||||
const data = {
|
||||
category: FilterCategory.METADATA,
|
||||
value: ['val'],
|
||||
key: '_source',
|
||||
operator: 'is',
|
||||
sourceOperator: 'is',
|
||||
source: '_source',
|
||||
custom: false,
|
||||
isEvent: false,
|
||||
filters: null,
|
||||
};
|
||||
const mapped = filterMap(data as any);
|
||||
expect(mapped.type).toBe(FilterKey.METADATA);
|
||||
expect(mapped.source).toBe('source');
|
||||
expect(mapped.value).toEqual(['val']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchStore class', () => {
|
||||
let store: SearchStore;
|
||||
beforeEach(() => {
|
||||
store = new SearchStore();
|
||||
mockSessionFetch.mockClear();
|
||||
});
|
||||
|
||||
it('applySavedSearch sets filters', () => {
|
||||
const saved = new SavedSearch({
|
||||
name: 'test',
|
||||
filter: { filters: [{ key: FilterKey.USERID, value: ['123'], operator: 'is' }] },
|
||||
});
|
||||
store.applySavedSearch(saved);
|
||||
expect(store.savedSearch).toBe(saved);
|
||||
expect(store.instance.filters.length).toBe(1);
|
||||
// @ts-ignore
|
||||
expect(store.instance.filters[0].key).toBe(FilterKey.USERID);
|
||||
});
|
||||
|
||||
it('addFilterByKeyAndValue adds filter and triggers fetch', () => {
|
||||
store.addFilterByKeyAndValue(FilterKey.USERID, ['42']);
|
||||
expect(store.instance.filters.length).toBe(1);
|
||||
expect(mockSessionFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetchSessions applies duration filter from settings', async () => {
|
||||
mockSettingsStore.sessionSettings.durationFilter = { operator: '<', count: 1, countType: 'sec' };
|
||||
await store.fetchSessions();
|
||||
const call = mockSessionFetch.mock.calls[0][0];
|
||||
const duration = call.filters.find((f: any) => f.type === FilterKey.DURATION);
|
||||
expect(duration).toBeTruthy();
|
||||
expect(duration.value).toEqual([1000, 0]);
|
||||
});
|
||||
});
|
||||
355
frontend/tests/sessionStore.test.ts
Normal file
355
frontend/tests/sessionStore.test.ts
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
import { sessionService } from '../app/services';
|
||||
import Session from '../app/types/session'
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import SessionStore from '../app/mstore/sessionStore';
|
||||
import { searchStore } from '../app/mstore/index';
|
||||
import { checkEventWithFilters } from '../app/components/Session_/Player/Controls/checkEventWithFilters';
|
||||
import { mockSession } from './mocks/sessionData';
|
||||
|
||||
jest.mock('../app/player', () => ({
|
||||
createWebPlayer: jest.fn(),
|
||||
createIOSPlayer: jest.fn(),
|
||||
createClickMapPlayer: jest.fn(),
|
||||
createLiveWebPlayer: jest.fn(),
|
||||
createClipPlayer: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../app/services', () => ({
|
||||
sessionService: {
|
||||
getSessions: jest.fn(),
|
||||
getLiveSessions: jest.fn(),
|
||||
getSessionInfo: jest.fn(),
|
||||
getSessionEvents: jest.fn(),
|
||||
getSessionNotes: jest.fn(),
|
||||
getFavoriteSessions: jest.fn(),
|
||||
getSessionClickMap: jest.fn(),
|
||||
toggleFavorite: jest.fn(),
|
||||
getClickMap: jest.fn(),
|
||||
getAutoplayList: jest.fn(),
|
||||
getFirstMobUrl: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('App/player/web/network/loadFiles', () => ({
|
||||
loadFile: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/components/Session_/Player/Controls/checkEventWithFilters',
|
||||
() => ({
|
||||
checkEventWithFilters: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('../app/mstore/index', () => ({
|
||||
searchStore: {
|
||||
instance: {
|
||||
filters: [],
|
||||
events: [],
|
||||
toSearch: jest.fn().mockReturnValue({}),
|
||||
},
|
||||
},
|
||||
searchStoreLive: {
|
||||
instance: {
|
||||
filters: [],
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SessionStore', () => {
|
||||
let sessionStore: SessionStore;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sessionStore = new SessionStore();
|
||||
});
|
||||
|
||||
describe('resetUserFilter', () => {
|
||||
it('should reset user filter', () => {
|
||||
sessionStore.userFilter.update('page', 5);
|
||||
expect(sessionStore.userFilter.page).toBe(5);
|
||||
|
||||
sessionStore.resetUserFilter();
|
||||
expect(sessionStore.userFilter.page).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLiveSessions', () => {
|
||||
it('should fetch and set live sessions', async () => {
|
||||
const mockResponse = {
|
||||
sessions: [{ sessionId: 'live-1', userId: 'user1' }],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
(sessionService.getLiveSessions as jest.Mock).mockResolvedValue(
|
||||
mockResponse,
|
||||
);
|
||||
|
||||
await sessionStore.fetchLiveSessions({ sort: 'timestamp' });
|
||||
|
||||
expect(sessionService.getLiveSessions).toHaveBeenCalledWith({
|
||||
sort: 'timestamp',
|
||||
});
|
||||
expect(sessionStore.liveSessions.length).toBe(1);
|
||||
expect(sessionStore.liveSessions[0].sessionId).toBe('live-1');
|
||||
expect(sessionStore.liveSessions[0].live).toBe(true);
|
||||
expect(sessionStore.totalLiveSessions).toBe(1);
|
||||
expect(sessionStore.loadingLiveSessions).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle duration sort by converting to timestamp', async () => {
|
||||
(sessionService.getLiveSessions as jest.Mock).mockResolvedValue({
|
||||
sessions: [],
|
||||
total: 0,
|
||||
});
|
||||
|
||||
await sessionStore.fetchLiveSessions({ sort: 'duration', order: 'asc' });
|
||||
|
||||
expect(sessionService.getLiveSessions).toHaveBeenCalledWith({
|
||||
sort: 'timestamp',
|
||||
order: 'desc',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors and set loading to false', async () => {
|
||||
const mockError = new Error('API error');
|
||||
console.error = jest.fn();
|
||||
|
||||
(sessionService.getLiveSessions as jest.Mock).mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
await sessionStore.fetchLiveSessions();
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(mockError);
|
||||
expect(sessionStore.loadingLiveSessions).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSessions', () => {
|
||||
it('should fetch and set sessions', async () => {
|
||||
const mockResponse = {
|
||||
sessions: [
|
||||
new Session(mockSession({ sessionId: '1', favorite: true })),
|
||||
new Session(mockSession({ sessionId: '2' })),
|
||||
],
|
||||
total: 2,
|
||||
};
|
||||
|
||||
(sessionService.getSessions as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
await sessionStore.fetchSessions({ page: 1, filters: [] }, true);
|
||||
|
||||
expect(sessionService.getSessions).toHaveBeenCalledWith({
|
||||
page: 1,
|
||||
filters: [],
|
||||
});
|
||||
expect(sessionStore.list.length).toBe(2);
|
||||
expect(sessionStore.total).toBe(2);
|
||||
expect(sessionStore.sessionIds).toEqual(['1', '2']);
|
||||
expect(sessionStore.favoriteList.length).toBe(1);
|
||||
expect(sessionStore.favoriteList[0].sessionId).toBe('1');
|
||||
expect(sessionStore.loadingSessions).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle errors and set loading to false', async () => {
|
||||
const mockError = new Error('API error');
|
||||
console.error = jest.fn();
|
||||
|
||||
(sessionService.getSessions as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await sessionStore.fetchSessions({ filters: [] }, true);
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(mockError);
|
||||
expect(sessionStore.loadingSessions).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAll', () => {
|
||||
it('should clear session list and current session', () => {
|
||||
sessionStore.list = [new Session({ sessionId: 'test' })];
|
||||
sessionStore.current = new Session({ sessionId: 'current' });
|
||||
|
||||
sessionStore.clearAll();
|
||||
|
||||
expect(sessionStore.list).toEqual([]);
|
||||
expect(sessionStore.current.sessionId).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSessionData', () => {
|
||||
it('should fetch and set session data with events', async () => {
|
||||
const mockSessionId = 'test-session-id';
|
||||
const mockSessionInfo = { sessionId: mockSessionId, userId: 'user1' };
|
||||
const mockEventsData = {
|
||||
events: [
|
||||
{ type: 'LOCATION', url: 'https://example.com', time: 100 },
|
||||
{ type: 'CLICK', value: 'button', time: 200 },
|
||||
],
|
||||
errors: [],
|
||||
crashes: [],
|
||||
issues: [],
|
||||
resources: [],
|
||||
stackEvents: [],
|
||||
userEvents: [],
|
||||
userTesting: [],
|
||||
};
|
||||
|
||||
(sessionService.getSessionInfo as jest.Mock).mockResolvedValue(
|
||||
mockSessionInfo,
|
||||
);
|
||||
(sessionService.getSessionEvents as jest.Mock).mockResolvedValue(
|
||||
mockEventsData,
|
||||
);
|
||||
(checkEventWithFilters as jest.Mock).mockReturnValue(false);
|
||||
|
||||
await sessionStore.fetchSessionData(mockSessionId);
|
||||
|
||||
expect(sessionService.getSessionInfo).toHaveBeenCalledWith(
|
||||
mockSessionId,
|
||||
false,
|
||||
);
|
||||
expect(sessionService.getSessionEvents).toHaveBeenCalledWith(
|
||||
mockSessionId,
|
||||
);
|
||||
expect(sessionStore.current.sessionId).toBe(mockSessionId);
|
||||
expect(sessionStore.visitedEvents.length).toBe(1);
|
||||
expect(sessionStore.visitedEvents[0].url).toBe('https://example.com');
|
||||
expect(sessionStore.host).toBe('');
|
||||
expect(sessionStore.prefetched).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle event filtering based on search filters', async () => {
|
||||
const mockSessionId = 'test-session-id';
|
||||
const mockSessionInfo = { sessionId: mockSessionId, userId: 'user1' };
|
||||
const mockEventsData = {
|
||||
events: [
|
||||
{ type: 'LOCATION', url: 'https://example.com', time: 100 },
|
||||
{ type: 'CLICK', value: 'button', time: 200 },
|
||||
],
|
||||
};
|
||||
|
||||
// Setup search filters
|
||||
(searchStore.instance.events as any) = [
|
||||
{ key: 'LOCATION', operator: 'is', value: 'https://example.com' },
|
||||
];
|
||||
|
||||
(sessionService.getSessionInfo as jest.Mock).mockResolvedValue(
|
||||
mockSessionInfo,
|
||||
);
|
||||
(sessionService.getSessionEvents as jest.Mock).mockResolvedValue(
|
||||
mockEventsData,
|
||||
);
|
||||
(checkEventWithFilters as jest.Mock).mockReturnValue(true);
|
||||
|
||||
await sessionStore.fetchSessionData(mockSessionId);
|
||||
|
||||
expect(sessionStore.eventsIndex).toEqual([0]);
|
||||
});
|
||||
|
||||
it('should handle different filter operators', async () => {
|
||||
const mockSessionId = 'test-session-id';
|
||||
const mockSessionInfo = { sessionId: mockSessionId, userId: 'user1' };
|
||||
const mockEventsData = {
|
||||
events: [
|
||||
{ type: 'LOCATION', url: 'https://example.com', time: 100 },
|
||||
{ type: 'CLICK', value: 'test-button', time: 200 },
|
||||
],
|
||||
};
|
||||
|
||||
(searchStore.instance.events as any) = [
|
||||
{ key: 'CLICK', operator: 'contains', value: 'test' },
|
||||
];
|
||||
|
||||
(sessionService.getSessionInfo as jest.Mock).mockResolvedValue(
|
||||
mockSessionInfo,
|
||||
);
|
||||
(sessionService.getSessionEvents as jest.Mock).mockResolvedValue(
|
||||
mockEventsData,
|
||||
);
|
||||
(checkEventWithFilters as jest.Mock).mockReturnValue(true);
|
||||
|
||||
await sessionStore.fetchSessionData(mockSessionId);
|
||||
|
||||
expect(sessionStore.eventsIndex).toEqual([1]);
|
||||
});
|
||||
|
||||
it('should handle errors when fetching events', async () => {
|
||||
const mockSessionId = 'test-session-id';
|
||||
const mockSessionInfo = { sessionId: mockSessionId, userId: 'user1' };
|
||||
const mockError = new Error('API error');
|
||||
console.error = jest.fn();
|
||||
|
||||
(sessionService.getSessionInfo as jest.Mock).mockResolvedValue(
|
||||
mockSessionInfo,
|
||||
);
|
||||
(sessionService.getSessionEvents as jest.Mock).mockRejectedValue(
|
||||
mockError,
|
||||
);
|
||||
|
||||
await sessionStore.fetchSessionData(mockSessionId);
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Failed to fetch events',
|
||||
mockError,
|
||||
);
|
||||
expect(sessionStore.current.sessionId).toBe(mockSessionId);
|
||||
expect(sessionStore.current.events).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle errors when fetching session info', async () => {
|
||||
const mockSessionId = 'test-session-id';
|
||||
const mockError = new Error('API error');
|
||||
console.error = jest.fn();
|
||||
|
||||
(sessionService.getSessionInfo as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await sessionStore.fetchSessionData(mockSessionId);
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(mockError);
|
||||
expect(sessionStore.fetchFailed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortSessions', () => {
|
||||
it('should sort sessions by the specified key in ascending order', () => {
|
||||
sessionStore.list = [
|
||||
new Session(mockSession({ duration: 3000, sessionId: '1' })),
|
||||
new Session(mockSession({ duration: 1000, sessionId: '2' })),
|
||||
new Session(mockSession({ duration: 2000, sessionId: '3' })),
|
||||
];
|
||||
sessionStore.favoriteList = [
|
||||
new Session(mockSession({ duration: 3000, sessionId: '1' })),
|
||||
new Session(mockSession({ duration: 2000, sessionId: '3' })),
|
||||
];
|
||||
|
||||
sessionStore.sortSessions('duration', 1);
|
||||
|
||||
expect(sessionStore.list.map((s) => s.sessionId)).toEqual([
|
||||
'2',
|
||||
'3',
|
||||
'1',
|
||||
]);
|
||||
expect(sessionStore.favoriteList.map((s) => s.sessionId)).toEqual([
|
||||
'3',
|
||||
'1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should sort sessions by the specified key in descending order', () => {
|
||||
sessionStore.list = [
|
||||
new Session(mockSession({ duration: 3000, sessionId: '1' })),
|
||||
new Session(mockSession({ duration: 1000, sessionId: '2' })),
|
||||
new Session(mockSession({ duration: 2000, sessionId: '3' })),
|
||||
];
|
||||
|
||||
sessionStore.sortSessions('duration', -1);
|
||||
expect(sessionStore.list.map((s) => s.sessionId)).toEqual([
|
||||
'1',
|
||||
'3',
|
||||
'2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,8 +7,11 @@ import {
|
|||
getResourceFromResourceTiming,
|
||||
getResourceFromNetworkRequest,
|
||||
} from '../app/player/web/types/resource';
|
||||
import type { ResourceTiming, NetworkRequest } from '../app/player/web/messages';
|
||||
import { test, describe, expect } from "@jest/globals";
|
||||
import type {
|
||||
ResourceTiming,
|
||||
NetworkRequest,
|
||||
} from '../app/player/web/messages';
|
||||
import { test, describe, expect } from '@jest/globals';
|
||||
|
||||
describe('getURLExtention', () => {
|
||||
test('should return the correct extension', () => {
|
||||
|
|
@ -21,20 +24,36 @@ describe('getURLExtention', () => {
|
|||
|
||||
describe('getResourceType', () => {
|
||||
test('should return the correct resource type based on initiator and URL', () => {
|
||||
expect(getResourceType('fetch', 'https://test.com')).toBe(ResourceType.FETCH);
|
||||
expect(getResourceType('beacon', 'https://test.com')).toBe(ResourceType.BEACON);
|
||||
expect(getResourceType('fetch', 'https://test.com')).toBe(
|
||||
ResourceType.FETCH,
|
||||
);
|
||||
expect(getResourceType('beacon', 'https://test.com')).toBe(
|
||||
ResourceType.BEACON,
|
||||
);
|
||||
expect(getResourceType('img', 'https://test.com')).toBe(ResourceType.IMG);
|
||||
expect(getResourceType('unknown', 'https://test.com/script.js')).toBe(ResourceType.SCRIPT);
|
||||
expect(getResourceType('unknown', 'https://test.com/style.css')).toBe(ResourceType.CSS);
|
||||
expect(getResourceType('unknown', 'https://test.com/image.png')).toBe(ResourceType.IMG);
|
||||
expect(getResourceType('unknown', 'https://test.com/video.mp4')).toBe(ResourceType.MEDIA);
|
||||
expect(getResourceType('unknown', 'https://test.com')).toBe(ResourceType.OTHER);
|
||||
expect(getResourceType('unknown', 'https://test.com/script.js')).toBe(
|
||||
ResourceType.SCRIPT,
|
||||
);
|
||||
expect(getResourceType('unknown', 'https://test.com/style.css')).toBe(
|
||||
ResourceType.CSS,
|
||||
);
|
||||
expect(getResourceType('unknown', 'https://test.com/image.png')).toBe(
|
||||
ResourceType.IMG,
|
||||
);
|
||||
expect(getResourceType('unknown', 'https://test.com/video.mp4')).toBe(
|
||||
ResourceType.MEDIA,
|
||||
);
|
||||
expect(getResourceType('unknown', 'https://test.com')).toBe(
|
||||
ResourceType.OTHER,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResourceName', () => {
|
||||
test('should return the last non-empty section of a URL', () => {
|
||||
expect(getResourceName('https://test.com/path/to/resource')).toBe('resource');
|
||||
expect(getResourceName('https://test.com/path/to/resource')).toBe(
|
||||
'resource',
|
||||
);
|
||||
expect(getResourceName('https://test.com/another/path/')).toBe('path');
|
||||
expect(getResourceName('https://test.com/singlepath')).toBe('singlepath');
|
||||
expect(getResourceName('https://test.com/')).toBe('test.com');
|
||||
|
|
@ -56,6 +75,7 @@ describe('Resource', () => {
|
|||
...testResource,
|
||||
name: 'script.js',
|
||||
isYellow: false,
|
||||
isRed: false,
|
||||
};
|
||||
expect(Resource(testResource)).toEqual(expectedResult);
|
||||
});
|
||||
|
|
@ -75,7 +95,7 @@ describe('getResourceFromResourceTiming', () => {
|
|||
initiator: 'fetch',
|
||||
transferredSize: 500,
|
||||
cached: false,
|
||||
time: 123
|
||||
time: 123,
|
||||
};
|
||||
const expectedResult = Resource({
|
||||
...testResourceTiming,
|
||||
|
|
@ -84,8 +104,20 @@ describe('getResourceFromResourceTiming', () => {
|
|||
success: true,
|
||||
status: '2xx-3xx',
|
||||
time: 123,
|
||||
timings: {
|
||||
contentDownload: undefined,
|
||||
dnsLookup: undefined,
|
||||
initialConnection: undefined,
|
||||
queueing: undefined,
|
||||
ssl: undefined,
|
||||
stalled: undefined,
|
||||
total: undefined,
|
||||
ttfb: 100,
|
||||
},
|
||||
});
|
||||
expect(getResourceFromResourceTiming(testResourceTiming, 0)).toEqual(expectedResult);
|
||||
expect(getResourceFromResourceTiming(testResourceTiming, 0)).toEqual(
|
||||
expectedResult,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -102,16 +134,19 @@ describe('getResourceFromNetworkRequest', () => {
|
|||
timestamp: 123,
|
||||
duration: 1,
|
||||
transferredBodySize: 100,
|
||||
time: 123
|
||||
time: 123,
|
||||
} as const;
|
||||
// @ts-ignore
|
||||
const expectedResult = Resource({
|
||||
...testNetworkRequest,
|
||||
success: true,
|
||||
status: '200',
|
||||
timings: {},
|
||||
time: 123,
|
||||
decodedBodySize: 100,
|
||||
});
|
||||
expect(getResourceFromNetworkRequest(testNetworkRequest, 0)).toEqual(expectedResult);
|
||||
expect(getResourceFromNetworkRequest(testNetworkRequest, 0)).toEqual(
|
||||
expectedResult,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const strings = [
|
|||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;700;900&display=swap');
|
||||
#login-required {
|
||||
color: #fff;
|
||||
}`,
|
||||
};`,
|
||||
`@import url("style.css") screen and (max-width: 600px);`
|
||||
];
|
||||
const testStrings = [
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./app/*"],
|
||||
"App/*": ["./app/*"],
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
"Shared/*": ["./app/components/shared/*"],
|
||||
"Player/*": ["./app/player/*"],
|
||||
"Player": ["./app/player"],
|
||||
"HOCs/*": ["./app/components/hocs/*"],
|
||||
"HOCs/*": ["./app/components/hocs/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
|
@ -36,6 +37,8 @@
|
|||
"app/**/*.tsx",
|
||||
"app/**/*.js",
|
||||
"app/**/*.jsx",
|
||||
"window.d.ts"
|
||||
"window.d.ts",
|
||||
"cypress/snapshots/sessionStore.test.ts",
|
||||
"tests/create.test.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@ant-design/colors@npm:^7.0.0, @ant-design/colors@npm:^7.2.0":
|
||||
version: 7.2.0
|
||||
resolution: "@ant-design/colors@npm:7.2.0"
|
||||
version: 7.2.1
|
||||
resolution: "@ant-design/colors@npm:7.2.1"
|
||||
dependencies:
|
||||
"@ant-design/fast-color": "npm:^2.0.6"
|
||||
checksum: 10c1/cf9eec1bf6ccc6f6757194dccdcc11f2dd84e14e8be2d3db6f85bca20e05432340a3df55632eed1d880bc8691efc1869fa0f18cb1f494aafb85b1565c71c2609
|
||||
checksum: 10c1/b8f3c98a55877da647fe4158e88600500713ba00d90ecd6a98c6cff068bd556771c833e65853d9e3443298ef114dd262f81b518c8b9fe364b80500c475c72788
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3038,61 +3038,61 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/browser-utils@npm:9.21.0":
|
||||
version: 9.21.0
|
||||
resolution: "@sentry-internal/browser-utils@npm:9.21.0"
|
||||
"@sentry-internal/browser-utils@npm:9.22.0":
|
||||
version: 9.22.0
|
||||
resolution: "@sentry-internal/browser-utils@npm:9.22.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:9.21.0"
|
||||
checksum: 10c1/a19819666dbd22148321c9e177f42dfc49c3779ed134076d91f005d2e8c4f90b5bef0214c305ffbdcfeccdcc0caca4f548707a8505af81a2b6ec2ee570da796c
|
||||
"@sentry/core": "npm:9.22.0"
|
||||
checksum: 10c1/526a908c4597d8d081dcaf87072f4d55bd438ea9f12a46417b81ebd5c44751a23cfe35f3f16e1805f9e7efd9b27b1848f6cbe3f75f8ba8115c67b10f53cba2e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/feedback@npm:9.21.0":
|
||||
version: 9.21.0
|
||||
resolution: "@sentry-internal/feedback@npm:9.21.0"
|
||||
"@sentry-internal/feedback@npm:9.22.0":
|
||||
version: 9.22.0
|
||||
resolution: "@sentry-internal/feedback@npm:9.22.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:9.21.0"
|
||||
checksum: 10c1/d12bfbb61abbcfddb67d42c7aaa1545e34f4df1210eb8dec3347eba7133e108b6b9cda680d5c131e7534882bb03c8c36972e9ab1396ca90d2113dd991d67138d
|
||||
"@sentry/core": "npm:9.22.0"
|
||||
checksum: 10c1/31e1d61691022678df190f69a9c81cbf1e6b9c3e8218681dc4e30e2dc75e1ec28ca1fda0bd4e13f92af473a68c797f03af3440bcefa16aff44cca5b8224373d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/replay-canvas@npm:9.21.0":
|
||||
version: 9.21.0
|
||||
resolution: "@sentry-internal/replay-canvas@npm:9.21.0"
|
||||
"@sentry-internal/replay-canvas@npm:9.22.0":
|
||||
version: 9.22.0
|
||||
resolution: "@sentry-internal/replay-canvas@npm:9.22.0"
|
||||
dependencies:
|
||||
"@sentry-internal/replay": "npm:9.21.0"
|
||||
"@sentry/core": "npm:9.21.0"
|
||||
checksum: 10c1/24ab33e4edf8aa7c9871eddd2492b080ad0ce40e0a04b8a11f99d4a034d8e34a632e155f380c24ea49104e666a12c26d544c623551a9ee21ef602023bac5c919
|
||||
"@sentry-internal/replay": "npm:9.22.0"
|
||||
"@sentry/core": "npm:9.22.0"
|
||||
checksum: 10c1/3108fe76bf0dd458b2e0ff748b42ccb59a28756720b2e55c1c1e44111bf93e4d4297a09b2cc08e6039abac5692bd2c33d79e7d859b31d142bd0128ee32738a30
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/replay@npm:9.21.0":
|
||||
version: 9.21.0
|
||||
resolution: "@sentry-internal/replay@npm:9.21.0"
|
||||
"@sentry-internal/replay@npm:9.22.0":
|
||||
version: 9.22.0
|
||||
resolution: "@sentry-internal/replay@npm:9.22.0"
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils": "npm:9.21.0"
|
||||
"@sentry/core": "npm:9.21.0"
|
||||
checksum: 10c1/9f5e9296b7026902e77714563933e40483c4fdcf9a02586af726b503a922d3df89bbe9ab7224edf532894e76bd3fdc63b280e4d417d06c8ebd19ca102863d6b5
|
||||
"@sentry-internal/browser-utils": "npm:9.22.0"
|
||||
"@sentry/core": "npm:9.22.0"
|
||||
checksum: 10c1/3e4b81005e6995437f842e05f0d13894e888ef4ef0b770f65e02d396985420192af0d69e613f4b9f96fb86d160bcd63b56874aba88022e28f394c8756c7cbda0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/browser@npm:^9.18.0":
|
||||
version: 9.21.0
|
||||
resolution: "@sentry/browser@npm:9.21.0"
|
||||
version: 9.22.0
|
||||
resolution: "@sentry/browser@npm:9.22.0"
|
||||
dependencies:
|
||||
"@sentry-internal/browser-utils": "npm:9.21.0"
|
||||
"@sentry-internal/feedback": "npm:9.21.0"
|
||||
"@sentry-internal/replay": "npm:9.21.0"
|
||||
"@sentry-internal/replay-canvas": "npm:9.21.0"
|
||||
"@sentry/core": "npm:9.21.0"
|
||||
checksum: 10c1/81a50d67a0e7bda344bc1fbaa4dad67061d38e1a0901c87df4938890361a4614e4830a4b886b3fcc2d0f869b686e7c0e5bb33d1fb2d7543fedecc7e6107a0fce
|
||||
"@sentry-internal/browser-utils": "npm:9.22.0"
|
||||
"@sentry-internal/feedback": "npm:9.22.0"
|
||||
"@sentry-internal/replay": "npm:9.22.0"
|
||||
"@sentry-internal/replay-canvas": "npm:9.22.0"
|
||||
"@sentry/core": "npm:9.22.0"
|
||||
checksum: 10c1/94e4b79cf04d6b4ee935d4200e26094b2e20f119ba54f2234ec78b5aea9499eefef8c82260846843c95fc8ecdff79435fcb7e1c6a054c157273304e545bbe94c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/core@npm:9.21.0":
|
||||
version: 9.21.0
|
||||
resolution: "@sentry/core@npm:9.21.0"
|
||||
checksum: 10c1/5bca1342cb4f113f524b0f31a3a0c8c1db448949ed79a517e0dc4a6c66516138034372bde78b6ab27a4694336773df3cc87ad96fdd4f6787dd9eb06b32dc6143
|
||||
"@sentry/core@npm:9.22.0":
|
||||
version: 9.22.0
|
||||
resolution: "@sentry/core@npm:9.22.0"
|
||||
checksum: 10c1/6203d394b62110219b6b751698e89d08ce6ab52dd90e1df74778a5cb966558334d4273aff4d4249249b76530856d5ae0c8ec2018193f096462e6d9ce753d4933
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3553,6 +3553,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jest@npm:^29.5.14":
|
||||
version: 29.5.14
|
||||
resolution: "@types/jest@npm:29.5.14"
|
||||
dependencies:
|
||||
expect: "npm:^29.0.0"
|
||||
pretty-format: "npm:^29.0.0"
|
||||
checksum: 10c1/645f1559cff204d5942980ecceca5d1eaaa2c6cb6c784bc60525bc195b5e7dd7342496740130d845b4227fa51c83ef2638a479eb06464b725c688d7254874133
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsdom@npm:^20.0.0":
|
||||
version: 20.0.1
|
||||
resolution: "@types/jsdom@npm:20.0.1"
|
||||
|
|
@ -3618,11 +3628,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:^22.7.8":
|
||||
version: 22.15.19
|
||||
resolution: "@types/node@npm:22.15.19"
|
||||
version: 22.15.21
|
||||
resolution: "@types/node@npm:22.15.21"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.21.0"
|
||||
checksum: 10c1/b57519cdfa277e62bfe38397f764239c24767740a7bf9999efb3972e626e1c5d9c305c89d8455ca20e34ce719746d6e1316de883af1b7a913fccb7ee60741e1a
|
||||
checksum: 10c1/dade6e3665167bd82d4ee2374f98eff78777c0ffa069b4c1146f3cbdacd7f412e9f1cb24e7d978b40fd832956121ba86acba0612cddbfe093bf660682f84eaeb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3710,11 +3720,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@types/react@npm:*, @types/react@npm:^19.0.10":
|
||||
version: 19.1.4
|
||||
resolution: "@types/react@npm:19.1.4"
|
||||
version: 19.1.5
|
||||
resolution: "@types/react@npm:19.1.5"
|
||||
dependencies:
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c1/725f4d9dbee82273ee358c2a5fbc634c847c7167118715ba5bb44e8f37c4c288fbfd3ada0b77b46af1792c33d97c97b006f5b557e9a1690631901cf3474f5aa7
|
||||
checksum: 10c1/3eaa6c8cad7ec0fce16a01c1c83cf44931ee0c09d4f5c9c20c650ecb42405bd8a800649342f3721a1f2cbd14393718e9fb72423f26fec5b01bc9a62688668846
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -8147,7 +8157,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expect@npm:^29.7.0":
|
||||
"expect@npm:^29.0.0, expect@npm:^29.7.0":
|
||||
version: 29.7.0
|
||||
resolution: "expect@npm:29.7.0"
|
||||
dependencies:
|
||||
|
|
@ -8822,11 +8832,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"get-tsconfig@npm:^4.10.0, get-tsconfig@npm:^4.7.0":
|
||||
version: 4.10.0
|
||||
resolution: "get-tsconfig@npm:4.10.0"
|
||||
version: 4.10.1
|
||||
resolution: "get-tsconfig@npm:4.10.1"
|
||||
dependencies:
|
||||
resolve-pkg-maps: "npm:^1.0.0"
|
||||
checksum: 10c1/9c77b946c79ede0940c3257ec02067b49faee25d7fb1f1376e3b7fab3b72250a33a4fdf9f970f79ceebedf04c92390f300d2647c91c2ce20695f4ed98ed90736
|
||||
checksum: 10c1/ef21fd27464678c41e7e6516103097e1fd327a8cb3f21b07cd20cdeb4a134030fd00b30d6bbac7ff8268b73f367a87e1e702faa5827442041c329ab959cd0c61
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -10732,7 +10742,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest@npm:^29.5.0":
|
||||
"jest@npm:^29.7.0":
|
||||
version: 29.7.0
|
||||
resolution: "jest@npm:29.7.0"
|
||||
dependencies:
|
||||
|
|
@ -12880,6 +12890,7 @@ __metadata:
|
|||
"@tanstack/react-query": "npm:^5.76.0"
|
||||
"@trivago/prettier-plugin-sort-imports": "npm:^4.3.0"
|
||||
"@types/eslint-plugin-jsx-a11y": "npm:^6"
|
||||
"@types/jest": "npm:^29.5.14"
|
||||
"@types/luxon": "npm:^3.4.2"
|
||||
"@types/node": "npm:^22.7.8"
|
||||
"@types/prismjs": "npm:^1"
|
||||
|
|
@ -12932,7 +12943,7 @@ __metadata:
|
|||
i18next: "npm:^24.2.2"
|
||||
i18next-browser-languagedetector: "npm:^8.1.0"
|
||||
immutable: "npm:^4.3.7"
|
||||
jest: "npm:^29.5.0"
|
||||
jest: "npm:^29.7.0"
|
||||
jest-environment-jsdom: "npm:^29.5.0"
|
||||
js-untar: "npm:^2.0.0"
|
||||
jspdf: "npm:^3.0.1"
|
||||
|
|
@ -12982,7 +12993,7 @@ __metadata:
|
|||
tailwindcss: "npm:^3.4.17"
|
||||
thread-loader: "npm:^4.0.4"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
ts-jest: "npm:^29.0.5"
|
||||
ts-jest: "npm:^29.3.3"
|
||||
ts-node: "npm:^10.7.0"
|
||||
typescript: "npm:^4.9.5"
|
||||
typescript-eslint: "npm:^8.32.1"
|
||||
|
|
@ -13999,7 +14010,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-format@npm:^29.7.0":
|
||||
"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0":
|
||||
version: 29.7.0
|
||||
resolution: "pretty-format@npm:29.7.0"
|
||||
dependencies:
|
||||
|
|
@ -17202,7 +17213,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-jest@npm:^29.0.5":
|
||||
"ts-jest@npm:^29.3.3":
|
||||
version: 29.3.4
|
||||
resolution: "ts-jest@npm:29.3.4"
|
||||
dependencies:
|
||||
|
|
@ -17972,12 +17983,12 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"watchpack@npm:^2.4.1":
|
||||
version: 2.4.3
|
||||
resolution: "watchpack@npm:2.4.3"
|
||||
version: 2.4.4
|
||||
resolution: "watchpack@npm:2.4.4"
|
||||
dependencies:
|
||||
glob-to-regexp: "npm:^0.4.1"
|
||||
graceful-fs: "npm:^4.1.2"
|
||||
checksum: 10c1/dadcdad5b7ef9aa7f94bde18560dd5fd1c81261ca07b444ae475c643931d2a838050a71e03705aa88cfb67097aa9ca61ac11b82c13085af66aeb5b1fa4340215
|
||||
checksum: 10c1/64e6d92b1d4728dd2ef20d56b26415a9565c47f3902462b171f85f3e996b1f3afe208b8f54fae414458841397655f9f22d474d7a6b2a998bfeda565e957f3b90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue