E2e tests frontend (#3471)

* add playwright

* add e2e test

* add test

* add e2e test

* add e2e tests
This commit is contained in:
Andrey Babushkin 2025-06-03 12:09:10 +02:00 committed by GitHub
parent 85f6551b32
commit 09c98b5bde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 541 additions and 109 deletions

View file

@ -215,6 +215,7 @@ export function GridItem({
className={`bg-white rounded-lg overflow-hidden shadow-sm border ${
isSelected ? 'border-teal/30' : ''
} transition flex flex-col items-start hover:border-teal`}
data-test-id="spot-list-item"
>
<div
className="relative group overflow-hidden"

View file

@ -110,7 +110,7 @@ function ProjectDropdown(props: { location: any }) {
}}
placement="bottomLeft"
>
<Button>
<Button data-test-id="project-dropdown" >
<Space>
<Text className="font-medium capitalize">
{showCurrent && activeSite ? (

View file

@ -143,6 +143,7 @@ function SelectDateRange(props: Props) {
isUSLocale={isUSLocale}
useButtonStyle={useButtonStyle}
isTileDisabled={isTileDisabled}
data-test-id="widget-select-date-range"
/>
);
}
@ -164,6 +165,7 @@ function SelectDateRange(props: Props) {
period={period}
right
style={{ width: '100%' }}
data-test-id="widget-select-date-range"
/>
{isCustom && (
<OutsideClickDetectingDiv
@ -272,6 +274,7 @@ function AndDateRange({
size="small"
className="flex items-center btn-card-period-range"
icon={useButtonStyle ? <Calendar size={16} /> : null}
data-test-id="widget-select-date-range"
>
{isCustomRange ? customRange : selectedValue?.label}
<DownOutlined />

View file

@ -27,7 +27,7 @@ function SessionHeader() {
};
return (
<div className="flex items-center px-4 py-3 justify-between w-full">
<div className="flex items-center px-4 py-3 justify-between w-full" data-test-id="session-list-header">
<div
className={`flex w-full flex-wrap gap-2 ${screens.md ? 'justify-between' : 'justify-start'}`}
>

View file

@ -31,5 +31,9 @@ export default {
transformIgnorePatterns: [
'/node_modules/(?!syncod)',
],
setupFiles: ['<rootDir>/tests/jest.setup.ts'],
setupFiles: ['<rootDir>/tests/unit/jest.setup.ts'],
testPathIgnorePatterns: [
'/node_modules/',
'/tests/playwright/'
],
};

View file

@ -29,6 +29,7 @@
"@eslint/js": "^9.26.0",
"@medv/finder": "^4.0.2",
"@neodrag/react": "^2.3.0",
"@playwright/test": "^1.52.0",
"@sentry/browser": "^9.18.0",
"@svg-maps/world": "^1.0.1",
"@tanstack/react-query": "^5.76.0",
@ -65,6 +66,7 @@
"mobx": "^6.13.7",
"mobx-persist-store": "^1.1.8",
"mobx-react-lite": "^4.1.0",
"playwright": "^1.52.0",
"prismjs": "^1.30.0",
"rc-time-picker": "^3.7.3",
"react": "^19.1.0",

View file

@ -0,0 +1,37 @@
/* eslint-disable import/no-extraneous-dependencies */
import 'dotenv/config';
import { defineConfig, devices } from '@playwright/test';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/playwright',
fullyParallel: true,
retries: 0,
workers: 1,
reporter: 'list',
use: {
baseURL: 'http://localhost:3333',
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'yarn start',
url: 'http://localhost:3333',
timeout: 120 * 1000,
reuseExistingServer: true,
},
});

View file

@ -0,0 +1,70 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:3333",
"localStorage": [
{
"name": "__$session-timezone$_local__",
"value": "true"
},
{
"name": "i18nextLng",
"value": "en"
},
{
"name": "theme",
"value": "light"
},
{
"name": "__$user-gettingStarted$__",
"value": "{\"steps\":[{\"title\":\"🛠️ Install OpenReplay\",\"status\":\"completed\"},{\"title\":\"🕵️ Identify Users\",\"status\":\"completed\"},{\"title\":\"🧑‍💻 Invite Team Members\",\"status\":\"completed\"},{\"title\":\"🔌 Integrations\",\"status\":\"completed\"}],\"status\":\"completed\"}"
},
{
"name": "__$global-destinationPath$__",
"value": "/"
},
{
"name": "___$or_spotToken$___",
"value": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4ODc2MzkyLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODg3NTc5MiwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0.5Xuboo2h30P6mTYHEtzoaJTBZvZGwEMs8ywookDsY0Xp0Ah9m9K-s3WF2x-M_7LCfDOp7nBFa8j9AyKz09V0oA"
},
{
"name": "__$session-timezone$__",
"value": "{\"label\":\"UTC +02:00\",\"value\":\"UTC+02\"}"
},
{
"name": "__$session-mouseTrail$__",
"value": "true"
},
{
"name": "__openreplay_health_status",
"value": "1748875801944"
},
{
"name": "__$user-siteId$__",
"value": "109"
},
{
"name": "__or__langBannerClosed",
"value": "0"
},
{
"name": "AuthStore",
"value": "{\"authDetails\":\"{\\\"tenants\\\":true,\\\"sso\\\":null,\\\"ssoProvider\\\":null,\\\"enforceSSO\\\":null,\\\"edition\\\":\\\"foss\\\"}\",\"__mps__\":{\"expireInTimestamp\":1748879400231}}"
},
{
"name": "UserStore",
"value": "{\"siteId\":null,\"tenants\":[],\"jwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4OTYyMTkyLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODg3NTc5MiwiYXVkIjoiZnJvbnQ6T3BlblJlcGxheSJ9.bfMw80k15BIwHkR_JQsY_DFqDJwERZcpYLOBRbcPcm2OT_WPozDal6HS8rs5YeyW0m98HRJa1ShGoMiyQhxMJA\",\"spotJwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4ODc2MzkyLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODg3NTc5MiwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0.5Xuboo2h30P6mTYHEtzoaJTBZvZGwEMs8ywookDsY0Xp0Ah9m9K-s3WF2x-M_7LCfDOp7nBFa8j9AyKz09V0oA\",\"scopeState\":2,\"onboarding\":false,\"account\":\"{\\\"id\\\":58,\\\"email\\\":\\\"andrei@openreplay.com\\\",\\\"smtp\\\":false,\\\"expirationDate\\\":-1,\\\"permissions\\\":[],\\\"settings\\\":{\\\"modules\\\":[\\\"usability-tests\\\",\\\"feature-flags\\\"]},\\\"iceServers\\\":[],\\\"hasPassword\\\":true,\\\"apiKey\\\":\\\"48Vph82zUEWHmfPSUbgG\\\",\\\"edition\\\":\\\"foss\\\",\\\"optOut\\\":false,\\\"versionNumber\\\":\\\"1.17.0\\\",\\\"name\\\":\\\"Andrei\\\",\\\"createdAt\\\":1652690354756,\\\"admin\\\":true,\\\"superAdmin\\\":false}\"}"
},
{
"name": "__openreplay_health_response",
"value": "{\"overallHealth\":true,\"healthMap\":{\"databases\":{\"name\":\"Databases\",\"healthOk\":true,\"subservices\":{\"postgres\":{\"health\":true,\"details\":{}}},\"serviceName\":\"databases\"},\"ingestionPipeline\":{\"name\":\"Ingestion Pipeline\",\"healthOk\":true,\"subservices\":{\"redis\":{\"health\":true,\"details\":{}}},\"serviceName\":\"ingestionPipeline\"},\"backendServices\":{\"name\":\"Backend Services\",\"healthOk\":true,\"subservices\":{\"alerts\":{\"health\":true,\"details\":{}},\"assets\":{\"health\":true,\"details\":{}},\"assist\":{\"health\":true,\"details\":{}},\"chalice\":{\"health\":true,\"details\":{}},\"db\":{\"health\":true,\"details\":{}},\"ender\":{\"health\":true,\"details\":{}},\"frontend\":{\"health\":true,\"details\":{}},\"heuristics\":{\"health\":true,\"details\":{}},\"http\":{\"health\":true,\"details\":{}},\"ingress-nginx\":{\"health\":true,\"details\":{}},\"integrations\":{\"health\":true,\"details\":{}},\"sink\":{\"health\":true,\"details\":{}},\"sourcemapreader\":{\"health\":true,\"details\":{}},\"storage\":{\"health\":true,\"details\":{}}},\"serviceName\":\"backendServices\"}},\"details\":{\"numberOfSessionsCaptured\":216638,\"numberOfEventCaptured\":1840149}}"
},
{
"name": "__$session-filter$__",
"value": "{\"name\":\"\",\"events\":[],\"custom\":{},\"rangeValue\":\"LAST_24_HOURS\",\"startDate\":1748790000000,\"endDate\":1748876400000,\"groupByUser\":false,\"sort\":\"startTs\",\"order\":\"desc\",\"strict\":false,\"eventsOrder\":\"then\",\"limit\":10,\"page\":1,\"perPage\":10,\"tab\":\"sessions\",\"filters\":[]}"
}
]
}
]
}

View file

@ -0,0 +1,38 @@
import { authStateFile, testUseAuthState } from './helpers';
import { expect, test as setup } from '@playwright/test';
testUseAuthState();
setup.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3333');
});
setup('authenticate', async ({ page }) => {
await page.goto('/login');
try {
const url = page.url();
console.log('Current URL:', url);
if (url.includes('login')) {
console.log('Already on login page, skipping authentication');
await page.locator('[data-test-id="login"]').click();
await page.locator('.ant-input-affix-wrapper').first().click();
await page
.locator('[data-test-id="login"]')
.fill('andrei@openreplay.com');
await page.locator('[data-test-id="password"]').click();
await page.locator('[data-test-id="password"]').fill('Andrey123!');
await page.locator('[data-test-id="log-button"]').click();
}
await expect(page.getByRole('heading', { name: 'Sessions' })).toBeVisible();
} catch (e) {
console.error('Error during authentication:', e);
}
try {
await page.context().storageState({ path: authStateFile });
} catch (e) {
console.error('Error saving authentication state:', e);
}
});

View file

@ -0,0 +1,11 @@
import { test, expect } from '@playwright/test';
test('Check if dashboards exist', async ({ page }) => {
await page.goto('http://localhost:3333/login');
await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com');
await page.locator('[data-test-id="password"]').fill('Andrey123!');
await page.locator('[data-test-id="log-button"]').click();
await page.getByText('Dashboards').click();
await page.getByText('Renamed One').click();
await expect(page.getByRole('heading', { name: 'Renamed One' })).toBeVisible();
});

View file

@ -0,0 +1,18 @@
import { mkdirSync } from "fs";
import { exists } from "i18next";
import { dirname } from "path";
import { test } from "@playwright/test";
export const authStateFile = 'tests/playwright/auth-state.json';
mkdirSync(dirname(authStateFile), { recursive: true });
export function testUseAuthState() {
if (exists(authStateFile)) {
test.use({ storageState: authStateFile });
}
test.afterEach(async ({ page }) => {
await page.context().storageState({ path: authStateFile });
})
}

View file

@ -0,0 +1,16 @@
import { test, expect } from '@playwright/test';
import { testUseAuthState } from './helpers';
testUseAuthState();
test('check session list after change period', async ({ page }) => {
await page.goto('http://localhost:3333/login');
await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com');
await page.locator('[data-test-id="password"]').fill('Andrey123!');
await page.locator('[data-test-id="log-button"]').click();
await page.getByRole('button', { name: 'Android caret-down' }).click();
await page.getByRole('menuitem', { name: 'OpenReplay Documentation Site' }).click();
await page.getByRole('button', { name: 'Past 24 Hours down' }).click();
await page.getByRole('menuitem', { name: 'Past 30 Days' }).click();
await page.locator('#session-item').first().click();
});

View file

@ -0,0 +1,9 @@
import { test, expect } from '@playwright/test';
test('Sign in flow', async ({ page }) => {
await page.goto('/');
await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com');
await page.locator('[data-test-id="password"]').fill('Andrey123!');
await page.locator('[data-test-id="log-button"]').click();
await expect(page.getByRole('heading', { name: 'Sessions' })).toBeVisible();
});

View file

@ -0,0 +1,14 @@
import { test, expect } from '@playwright/test';
test('Spots should display', async ({ page }) => {
await page.goto('http://localhost:3333/login');
await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com');
await page.locator('[data-test-id="password"]').fill('Andrey123!');
await page.locator('[data-test-id="log-button"]').click();
await page.getByText('Spots').click();
await page.waitForTimeout(1000);
const spotItems = (
await page.locator('[data-test-id="spot-list-item"]').all()
).length;
expect(spotItems).toBeGreaterThan(0);
});

View file

@ -0,0 +1,37 @@
import { test, expect } from '@playwright/test';
test('The freshest session from openreplay website doesnt have white screen', async ({ page }) => {
await page.goto('http://localhost:3333/login');
await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com');
await page.locator('[data-test-id="password"]').fill('Andrey123!');
await page.locator('[data-test-id="log-button"]').click();
await page.waitForTimeout(1000);
await page.locator('[data-test-id="session-list-header"]').locator('[data-test-id="widget-select-date-range"]').click();
await page.getByText('Past 30 Days').click();
await page.locator('[data-test-id="project-dropdown"]').click();
await page.getByRole('button', { name: 'Android caret-down' }).click();
await page.getByText('OpenReplay Documentation Site').click();
await page.waitForTimeout(1000);
const borderBlocks = await page.locator('.border-b').elementHandles();
if (borderBlocks.length >= 2) {
const secondBlock = borderBlocks[1];
const playButton = await secondBlock.$('#play-button');
if (playButton) {
const link = await playButton.$('a');
if (link) {
await link.click();
}
}
}
await page.waitForTimeout(1000);
const iframeElement = await page
.locator('iframe[class^="screen-module__iframe"]')
.first();
const frameHandle = await iframeElement.elementHandle();
const frame = await frameHandle?.contentFrame();
const hasBody = await frame?.evaluate(() => !!document.body);
expect(hasBody).toBeTruthy();
});

View file

@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach, jest } from '@jest/globals';
import ListWalker from '../app/player/common/ListWalker';
import type { Timed } from '../app/player/common/types';
import ListWalker from '../../app/player/common/ListWalker';
import type { Timed } from '../../app/player/common/types';
interface Item extends Timed {
value?: string;
@ -17,24 +17,24 @@ describe('ListWalker', () => {
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]);
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(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]);
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]);
expect(walker.list.map((i) => i.time)).toEqual([1, 2, 3]);
});
test('moveGetLast advances pointer and returns item', () => {

View file

@ -1,6 +1,6 @@
import { describe, test, expect } from '@jest/globals';
import MFileReader from '../app/player/web/messages/MFileReader';
import { MType } from '../app/player/web/messages/raw.gen';
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[] = [];

View file

@ -1,13 +1,13 @@
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
import MessageLoader from '../app/player/web/MessageLoader';
import { MType } from '../app/player/web/messages';
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', () => ({
jest.mock('../../app/player/web/network/loadFiles', () => ({
__esModule: true,
loadFiles: jest.fn(async () => {}),
requestTarball: jest.fn(),
@ -17,7 +17,7 @@ jest.mock('../app/player/web/network/loadFiles', () => ({
const decryptSessionBytesMock = jest.fn((b: Uint8Array) => Promise.resolve(b));
jest.mock('../app/player/web/network/crypto', () => ({
jest.mock('../../app/player/web/network/crypto', () => ({
__esModule: true,
decryptSessionBytes: jest.fn((b: Uint8Array) => Promise.resolve(b)),
}));
@ -32,11 +32,11 @@ jest.mock('Player/common/tarball', () => ({
default: jest.fn((b: Uint8Array) => b),
}));
import MFileReader from '../app/player/web/messages/MFileReader';
import MFileReader from '../../app/player/web/messages/MFileReader';
const readNextMock = jest.fn();
jest.mock('../app/player/web/messages/MFileReader', () => {
jest.mock('../../app/player/web/messages/MFileReader', () => {
return {
__esModule: true,
default: jest.fn().mockImplementation(() => {
@ -49,7 +49,7 @@ jest.mock('../app/player/web/messages/MFileReader', () => {
};
});
import { mockSession } from './mocks/sessionData';
import { mockSession } from '../mocks/sessionData';
const createStore = () => {
const state: Record<string, any> = {};

View file

@ -1,13 +1,17 @@
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';
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('@medv/finder', () => ({
default: jest.fn(() => 'mocked network-proxy content'),
}));
jest.mock('syncod', () => {
return {
Decoder: jest.fn().mockImplementation(() => ({ decode: jest.fn(), set: jest.fn() })),
Decoder: jest
.fn()
.mockImplementation(() => ({ decode: jest.fn(), set: jest.fn() })),
};
});
@ -35,7 +39,14 @@ beforeEach(() => {
tabNames: {},
eventCount: 0,
});
manager = new TabSessionManager(session, store as any, new FakeScreen() as any, 'tab1', setSize, 0);
manager = new TabSessionManager(
session,
store as any,
new FakeScreen() as any,
'tab1',
setSize,
0,
);
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
@ -65,14 +76,21 @@ it('resetMessageManagers should clear managers', () => {
});
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['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.performanceAvailability).toEqual({
cpu: true,
fps: true,
heap: false,
nodes: true,
});
expect(state.urlsList[0].url).toBe('http://example.com');
});

View file

@ -33,17 +33,17 @@ class MockWebLivePlayer {
) {}
}
jest.mock('../app/player/mobile/IOSPlayer', () => ({
jest.mock('../../app/player/mobile/IOSPlayer', () => ({
__esModule: true,
default: MockIOSPlayer,
}));
jest.mock('../app/player/web/WebPlayer', () => ({
jest.mock('../../app/player/web/WebPlayer', () => ({
__esModule: true,
default: MockWebPlayer,
}));
jest.mock('../app/player/web/WebLivePlayer', () => ({
jest.mock('../../app/player/web/WebLivePlayer', () => ({
__esModule: true,
default: MockWebLivePlayer,
}));
@ -54,7 +54,7 @@ import {
createClickMapPlayer,
createLiveWebPlayer,
createClipPlayer,
} from '../app/player/create';
} from '../../app/player/create';
const session = { id: 1 } as any;
const errorHandler = { error: jest.fn() };

View file

@ -1,28 +1,30 @@
import FeatureFlag, { Conditions, Variant } from '../app/mstore/types/FeatureFlag';
import FeatureFlag, {
Conditions,
Variant,
} from '../../app/mstore/types/FeatureFlag';
import { jest, test, expect, describe } from '@jest/globals';
jest.mock('App/mstore/types/filter', () => {
let filterData = { filters: [] }
let filterData = { filters: [] };
class MockFilter {
ID_KEY = "filterId"
filterId = ''
name = ''
filters = []
eventsOrder = 'then'
eventsOrderSupport = ['then', 'or', 'and']
startTimestamp = 0
endTimestamp = 0
ID_KEY = 'filterId';
filterId = '';
name = '';
filters = [];
eventsOrder = 'then';
eventsOrderSupport = ['then', 'or', 'and'];
startTimestamp = 0;
endTimestamp = 0;
fromJson(json) {
this.name = json.name
this.filters = json.filters.map((i) => i)
this.eventsOrder = json.eventsOrder
return this
this.name = json.name;
this.filters = json.filters.map((i) => i);
this.eventsOrder = json.eventsOrder;
return this;
}
}
return MockFilter
})
return MockFilter;
});
describe('Feature flag type test', () => {
// Test cases for Conditions class

View file

@ -37,9 +37,27 @@ 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: [] },
[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(() => []),
@ -61,17 +79,21 @@ const mockSessionStore = {
jest.mock('App/services', () => ({
searchService: { fetchSavedSearch: jest.fn() },
sessionService: { getSessions: jest.fn().mockResolvedValue({ sessions: [], total: 0 }) },
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';
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', () => {
@ -113,7 +135,9 @@ describe('SearchStore class', () => {
it('applySavedSearch sets filters', () => {
const saved = new SavedSearch({
name: 'test',
filter: { filters: [{ key: FilterKey.USERID, value: ['123'], operator: 'is' }] },
filter: {
filters: [{ key: FilterKey.USERID, value: ['123'], operator: 'is' }],
},
});
store.applySavedSearch(saved);
expect(store.savedSearch).toBe(saved);
@ -129,10 +153,16 @@ describe('SearchStore class', () => {
});
it('fetchSessions applies duration filter from settings', async () => {
mockSettingsStore.sessionSettings.durationFilter = { operator: '<', count: 1, countType: 'sec' };
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);
const duration = call.filters.find(
(f: any) => f.type === FilterKey.DURATION,
);
expect(duration).toBeTruthy();
expect(duration.value).toEqual([1000, 0]);
});

View file

@ -1,10 +1,10 @@
import { describe, expect, test } from '@jest/globals';
import Session from '../app/types/session';
import { Click, Location } from '../app/types/session/event';
import Issue from '../app/types/session/issue';
import { session } from './mocks/sessionResponse';
import { issues, events } from "./mocks/sessionData";
import Session from '../../app/types/session';
import { Click, Location } from '../../app/types/session/event';
import Issue from '../../app/types/session/issue';
import { session } from '../mocks/sessionResponse';
import { issues, events } from '../mocks/sessionData';
describe('Testing Session class', () => {
const sessionInfo = new Session(session.data);
@ -27,6 +27,6 @@ describe('Testing Session class', () => {
expect([...sessionInfo.issues]).toMatchObject(issues);
});
test('checking events mapping', () => {
expect([...sessionInfo.events.slice(0, 10)]).toMatchObject(events)
})
expect([...sessionInfo.events.slice(0, 10)]).toMatchObject(events);
});
});

View file

@ -1,20 +1,20 @@
import { sessionService } from '../app/services';
import Session from '../app/types/session'
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';
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', () => ({
jest.mock('../../app/player', () => ({
createWebPlayer: jest.fn(),
createIOSPlayer: jest.fn(),
createClickMapPlayer: jest.fn(),
createLiveWebPlayer: jest.fn(),
createClipPlayer: jest.fn()
createClipPlayer: jest.fn(),
}));
jest.mock('../app/services', () => ({
jest.mock('../../app/services', () => ({
sessionService: {
getSessions: jest.fn(),
getLiveSessions: jest.fn(),
@ -41,7 +41,7 @@ jest.mock(
}),
);
jest.mock('../app/mstore/index', () => ({
jest.mock('../../app/mstore/index', () => ({
searchStore: {
instance: {
filters: [],

View file

@ -2,7 +2,7 @@ import { jest, beforeEach, describe, expect, it } from '@jest/globals';
import spotPlayerStore, {
PANELS,
} from '../app/components/Spots/SpotPlayer/spotPlayerStore';
} from '../../app/components/Spots/SpotPlayer/spotPlayerStore';
jest.mock('App/player', () => ({
getResourceFromNetworkRequest: jest.fn(),

View file

@ -6,11 +6,11 @@ import {
Resource,
getResourceFromResourceTiming,
getResourceFromNetworkRequest,
} from '../app/player/web/types/resource';
} from '../../app/player/web/types/resource';
import type {
ResourceTiming,
NetworkRequest,
} from '../app/player/web/messages';
} from '../../app/player/web/messages';
import { test, describe, expect } from '@jest/globals';
describe('getURLExtention', () => {

View file

@ -1,10 +1,10 @@
import { test, describe, expect } from "@jest/globals";
import { resolveCSS } from '../app/player/web/messages/rewriter/urlResolve';
import { test, describe, expect } from '@jest/globals';
import { resolveCSS } from '../../app/player/web/messages/rewriter/urlResolve';
const strings = [
`@import "custom.css";`,
`@import url("chrome://communicator/skin/");`,
`@import '../app/custom.css';`,
`@import '../../app/custom.css';`,
`@import "styles/common.css";`,
`@import "/css/commonheader.css";`,
`@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;700;900&display=swap');`,
@ -13,7 +13,7 @@ const strings = [
#login-required {
color: #fff;
};`,
`@import url("style.css") screen and (max-width: 600px);`
`@import url("style.css") screen and (max-width: 600px);`,
];
const testStrings = [
`@import url("https://example.com/custom.css");`,
@ -27,12 +27,12 @@ const testStrings = [
#login-required {
color: #fff;
};`,
`@import url("https://example.com/style.css") screen and (max-width: 600px);`
]
`@import url("https://example.com/style.css") screen and (max-width: 600px);`,
];
describe('resolveCSS', () => {
test('should rewrite the CSS with the correct URLs', () => {
strings.forEach((string, i) => {
expect(resolveCSS('https://example.com', string)).toBe(testStrings[i]);
})
})
})
});
});
});

View file

@ -39,6 +39,6 @@
"app/**/*.jsx",
"window.d.ts",
"cypress/snapshots/sessionStore.test.ts",
"tests/create.test.ts"
"tests/unit/create.test.ts"
]
}

View file

@ -2900,6 +2900,17 @@ __metadata:
languageName: node
linkType: hard
"@playwright/test@npm:^1.52.0":
version: 1.52.0
resolution: "@playwright/test@npm:1.52.0"
dependencies:
playwright: "npm:1.52.0"
bin:
playwright: cli.js
checksum: 10c1/b7a57ce045e246d927cc8cd2091864313be6e778cc6e9f2484e6273ce091fd6eb8a68c177cf6cbd484dda68a4fa85dfaa8109ae2618475c0f07f6447b3988379
languageName: node
linkType: hard
"@rc-component/async-validator@npm:^5.0.3":
version: 5.0.4
resolution: "@rc-component/async-validator@npm:5.0.4"
@ -8680,6 +8691,16 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:2.3.2":
version: 2.3.2
resolution: "fsevents@npm:2.3.2"
dependencies:
node-gyp: "npm:latest"
checksum: 10c1/39f892d6e26b3d01f7e18fac9dd334d2c2a250ac2d534066e033dc5081c669b6c8b9e61f8ceb396fd9aafc210a2a3f56d8c9064768db1c1d2f34ad985c6d5b02
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
@ -8690,6 +8711,15 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
@ -12897,6 +12927,7 @@ __metadata:
"@medv/finder": "npm:^4.0.2"
"@neodrag/react": "npm:^2.3.0"
"@openreplay/sourcemap-uploader": "npm:^3.0.10"
"@playwright/test": "npm:^1.52.0"
"@sentry/browser": "npm:^9.18.0"
"@svg-maps/world": "npm:^1.0.1"
"@tanstack/react-query": "npm:^5.76.0"
@ -12969,6 +13000,7 @@ __metadata:
mobx-persist-store: "npm:^1.1.8"
mobx-react-lite: "npm:^4.1.0"
node-gyp: "npm:^9.0.0"
playwright: "npm:^1.52.0"
postcss: "npm:^8.5.3"
postcss-import: "npm:^16.1.0"
postcss-loader: "npm:^8.1.1"
@ -13428,6 +13460,30 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.52.0":
version: 1.52.0
resolution: "playwright-core@npm:1.52.0"
bin:
playwright-core: cli.js
checksum: 10c1/86d3a13d6a5e426757bfd620f87f52696cec42f1e6e39e40d595b8cbab602447d1e309ad67f51e03ced147677de7824518abb9587254fb14c8b4252b7b1bae2a
languageName: node
linkType: hard
"playwright@npm:1.52.0, playwright@npm:^1.52.0":
version: 1.52.0
resolution: "playwright@npm:1.52.0"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.52.0"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c1/58f5fa976f733fd64fba716f2e605cfd1fcdffc9f874f4baa657330ff9c4e9f8b1d49c02d33fbcea48c40472cc5ed07dec8d0291dac38cc2ed579449c0053ee5
languageName: node
linkType: hard
"plist@npm:^3.0.1":
version: 3.1.0
resolution: "plist@npm:3.1.0"

View file

@ -0,0 +1,66 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:3333",
"localStorage": [
{
"name": "__$session-timezone$_local__",
"value": "true"
},
{
"name": "i18nextLng",
"value": "en"
},
{
"name": "theme",
"value": "light"
},
{
"name": "__$user-gettingStarted$__",
"value": "{\"steps\":[{\"title\":\"🛠️ Install OpenReplay\",\"status\":\"completed\"},{\"title\":\"🕵️ Identify Users\",\"status\":\"completed\"},{\"title\":\"🧑‍💻 Invite Team Members\",\"status\":\"completed\"},{\"title\":\"🔌 Integrations\",\"status\":\"completed\"}],\"status\":\"completed\"}"
},
{
"name": "___$or_spotToken$___",
"value": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4OTQ1MTQxLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODk0NDU0MSwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0._QEHvIc8ShH0PsRPtDQAo50Dc-H-Adpu8CZKXQsPF31GSLUl5SS9MV92xntRxfcloigRA1Hz2F817EF5jrgNJg"
},
{
"name": "__$session-timezone$__",
"value": "{\"label\":\"UTC +02:00\",\"value\":\"UTC+02\"}"
},
{
"name": "__$session-mouseTrail$__",
"value": "true"
},
{
"name": "__openreplay_health_status",
"value": "1748944551307"
},
{
"name": "__$user-siteId$__",
"value": "65"
},
{
"name": "__or__langBannerClosed",
"value": "0"
},
{
"name": "AuthStore",
"value": "{\"authDetails\":\"{\\\"tenants\\\":true,\\\"sso\\\":null,\\\"ssoProvider\\\":null,\\\"enforceSSO\\\":null,\\\"edition\\\":\\\"foss\\\"}\",\"__mps__\":{\"expireInTimestamp\":1748948149584}}"
},
{
"name": "UserStore",
"value": "{\"siteId\":null,\"tenants\":[],\"jwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ5MDMwOTQxLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODk0NDU0MSwiYXVkIjoiZnJvbnQ6T3BlblJlcGxheSJ9.YHb2kldXFPzP2ecGoyPOo6I7_KH0BqhimOQKa1VtvSe_LTf2AzQNvKAYmsnx6-55lWX_b4wV5g4s4cdsYexOdw\",\"spotJwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4OTQ1MTQxLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODk0NDU0MSwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0._QEHvIc8ShH0PsRPtDQAo50Dc-H-Adpu8CZKXQsPF31GSLUl5SS9MV92xntRxfcloigRA1Hz2F817EF5jrgNJg\",\"scopeState\":2,\"onboarding\":false,\"account\":\"{\\\"id\\\":58,\\\"email\\\":\\\"andrei@openreplay.com\\\",\\\"smtp\\\":false,\\\"expirationDate\\\":-1,\\\"permissions\\\":[],\\\"settings\\\":{\\\"modules\\\":[\\\"usability-tests\\\",\\\"feature-flags\\\"]},\\\"iceServers\\\":[],\\\"hasPassword\\\":true,\\\"apiKey\\\":\\\"48Vph82zUEWHmfPSUbgG\\\",\\\"edition\\\":\\\"foss\\\",\\\"optOut\\\":false,\\\"versionNumber\\\":\\\"1.17.0\\\",\\\"name\\\":\\\"Andrei\\\",\\\"createdAt\\\":1652690354756,\\\"admin\\\":true,\\\"superAdmin\\\":false}\"}"
},
{
"name": "__openreplay_health_response",
"value": "{\"overallHealth\":true,\"healthMap\":{\"databases\":{\"name\":\"Databases\",\"healthOk\":true,\"subservices\":{\"postgres\":{\"health\":true,\"details\":{}}},\"serviceName\":\"databases\"},\"ingestionPipeline\":{\"name\":\"Ingestion Pipeline\",\"healthOk\":true,\"subservices\":{\"redis\":{\"health\":true,\"details\":{}}},\"serviceName\":\"ingestionPipeline\"},\"backendServices\":{\"name\":\"Backend Services\",\"healthOk\":true,\"subservices\":{\"alerts\":{\"health\":true,\"details\":{}},\"assets\":{\"health\":true,\"details\":{}},\"assist\":{\"health\":true,\"details\":{}},\"chalice\":{\"health\":true,\"details\":{}},\"db\":{\"health\":true,\"details\":{}},\"ender\":{\"health\":true,\"details\":{}},\"frontend\":{\"health\":true,\"details\":{}},\"heuristics\":{\"health\":true,\"details\":{}},\"http\":{\"health\":true,\"details\":{}},\"ingress-nginx\":{\"health\":true,\"details\":{}},\"integrations\":{\"health\":true,\"details\":{}},\"sink\":{\"health\":true,\"details\":{}},\"sourcemapreader\":{\"health\":true,\"details\":{}},\"storage\":{\"health\":true,\"details\":{}}},\"serviceName\":\"backendServices\"}},\"details\":{\"numberOfSessionsCaptured\":216813,\"numberOfEventCaptured\":1841202}}"
},
{
"name": "__$session-filter$__",
"value": "{\"name\":\"\",\"events\":[],\"custom\":{},\"rangeValue\":\"LAST_30_DAYS\",\"startDate\":1746352800000,\"endDate\":1748944800000,\"groupByUser\":false,\"sort\":\"startTs\",\"order\":\"desc\",\"strict\":false,\"eventsOrder\":\"then\",\"limit\":10,\"rangeName\":\"LAST_30_DAYS\",\"page\":1,\"perPage\":10,\"tab\":\"sessions\",\"filters\":[{\"type\":\"location\",\"isEvent\":true,\"value\":[\"\"],\"operator\":\"isAny\",\"source\":\"\",\"sourceOperator\":\"\",\"filters\":[]}]}"
}
]
}
]
}