change(ui): squash~ add unit tests and visual testing for replayer and session class

This commit is contained in:
nick-delirium 2023-01-18 19:49:01 +01:00 committed by Delirium
parent 9b9e268c1f
commit e346541411
24 changed files with 13272 additions and 23 deletions

3
frontend/.gitignore vendored
View file

@ -15,3 +15,6 @@ app/components/ui/SVG.js
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
*.env.json
cypress.env.json
cypress/snapshots/__diff_output__/

View file

@ -67,9 +67,13 @@ function WebPlayer(props: any) {
} }
const jumpToTime = props.query.get('jumpto'); const jumpToTime = props.query.get('jumpto');
const freeze = props.query.get('freeze')
if (jumpToTime) { if (jumpToTime) {
WebPlayerInst.jump(parseInt(jumpToTime)); WebPlayerInst.jump(parseInt(jumpToTime));
} }
if (freeze) {
WebPlayerInst.freeze()
}
return () => WebPlayerInst.clean(); return () => WebPlayerInst.clean();
}, [session.sessionId]); }, [session.sessionId]);

View file

@ -26,6 +26,7 @@ export interface SetState {
completed: boolean completed: boolean
live: boolean live: boolean
livePlay: boolean livePlay: boolean
freeze: boolean
endTime: number endTime: number
} }
@ -46,6 +47,7 @@ export default class Animator {
completed: false, completed: false,
live: false, live: false,
livePlay: false, livePlay: false,
freeze: false,
endTime: 0, endTime: 0,
} as const } as const
@ -129,6 +131,7 @@ export default class Animator {
} }
play() { play() {
if (this.store.get().freeze) return;
if (!this.store.get().ready) { if (!this.store.get().ready) {
cancelAnimationFrame(this.animationFrameRequestId) cancelAnimationFrame(this.animationFrameRequestId)
this.store.update({ playing: true }) this.store.update({ playing: true })
@ -145,6 +148,18 @@ export default class Animator {
this.store.update({ playing: false }) this.store.update({ playing: false })
} }
freeze() {
if (this.store.get().ready) {
// making sure that replay is displayed completely
setTimeout(() => {
this.store.update({ freeze: true })
this.pause()
}, 500)
} else {
setTimeout(() => this.freeze(), 500)
}
}
togglePlay = () => { togglePlay = () => {
const { playing, completed } = this.store.get() const { playing, completed } = this.store.get()
if (playing) { if (playing) {
@ -189,14 +204,4 @@ export default class Animator {
); );
} }
} }
// TODO: clearify logic of live time-travel
jumpToLive = () => {
cancelAnimationFrame(this.animationFrameRequestId)
this.setTime(this.store.get().endTime)
this.startAnimation()
this.store.update({ livePlay: true })
}
} }

View file

@ -119,7 +119,8 @@ export default class MessageManager {
private readonly session: any /*Session*/, private readonly session: any /*Session*/,
private readonly state: Store<State>, private readonly state: Store<State>,
private readonly screen: Screen, private readonly screen: Screen,
initialLists?: Partial<InitialLists> initialLists?: Partial<InitialLists>,
coldStart?: boolean
) { ) {
this.pagesManager = new PagesManager(screen, this.session.isMobile, cssLoading => { this.pagesManager = new PagesManager(screen, this.session.isMobile, cssLoading => {
screen.displayFrame(!cssLoading) screen.displayFrame(!cssLoading)
@ -138,8 +139,9 @@ export default class MessageManager {
this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live
if (!coldStart) {
this.loadMessages() this.loadMessages()
}
} }
private _sortMessagesHack(msgs: Message[]) { private _sortMessagesHack(msgs: Message[]) {

View file

@ -57,7 +57,9 @@ export default class Screen {
private readonly screen: HTMLDivElement; private readonly screen: HTMLDivElement;
private parentElement: HTMLElement | null = null; private parentElement: HTMLElement | null = null;
constructor(isMobile: boolean) { constructor(isMobile: boolean, coldStart?: boolean) {
if (coldStart) return;
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.className = styles.iframe; iframe.className = styles.iframe;
this.iframe = iframe; this.iframe = iframe;

View file

@ -25,7 +25,7 @@ export default class WebPlayer extends Player {
private targetMarker: TargetMarker private targetMarker: TargetMarker
constructor(protected wpState: Store<typeof WebPlayer.INITIAL_STATE>, session: any, live: boolean) { constructor(protected wpState: Store<typeof WebPlayer.INITIAL_STATE>, session: any, live: boolean, coldStart?: boolean) {
let initialLists = live ? {} : { let initialLists = live ? {} : {
event: session.events || [], event: session.events || [],
stack: session.stackEvents || [], stack: session.stackEvents || [],
@ -40,8 +40,8 @@ export default class WebPlayer extends Player {
) || [], ) || [],
} }
const screen = new Screen(session.isMobile) const screen = new Screen(session.isMobile, coldStart)
const messageManager = new MessageManager(session, wpState, screen, initialLists) const messageManager = new MessageManager(session, wpState, screen, initialLists, coldStart)
super(wpState, messageManager) super(wpState, messageManager)
this.screen = screen this.screen = screen
this.messageManager = messageManager this.messageManager = messageManager

View file

@ -83,13 +83,13 @@ class Console extends Event {
} }
} }
class Click extends Event { export class Click extends Event {
readonly type: typeof CLICKRAGE | typeof CLICK = CLICK; readonly type: typeof CLICKRAGE | typeof CLICK = CLICK;
readonly name = 'Click' readonly name = 'Click'
targetContent = ''; targetContent = '';
count: number count: number
constructor(evt: ClickEvent, isClickRage: boolean) { constructor(evt: ClickEvent, isClickRage?: boolean) {
super(evt); super(evt);
this.targetContent = evt.targetContent this.targetContent = evt.targetContent
this.count = evt.count this.count = evt.count
@ -116,7 +116,6 @@ export class Location extends Event {
readonly type = LOCATION; readonly type = LOCATION;
url: LocationEvent["url"] url: LocationEvent["url"]
host: LocationEvent["host"]; host: LocationEvent["host"];
pageLoad: LocationEvent["pageLoad"];
fcpTime: LocationEvent["fcpTime"]; fcpTime: LocationEvent["fcpTime"];
loadTime: LocationEvent["loadTime"]; loadTime: LocationEvent["loadTime"];
domContentLoadedTime: LocationEvent["domContentLoadedTime"]; domContentLoadedTime: LocationEvent["domContentLoadedTime"];

View file

@ -1,3 +1,4 @@
// @ts-nocheck
import JSBI from 'jsbi'; import JSBI from 'jsbi';
import chroma from 'chroma-js'; import chroma from 'chroma-js';
import * as htmlToImage from 'html-to-image'; import * as htmlToImage from 'html-to-image';

View file

@ -0,0 +1,11 @@
import { defineConfig } from "cypress";
import {addMatchImageSnapshotPlugin} from 'cypress-image-snapshot/plugin';
export default defineConfig({
e2e: {
baseUrl: 'http://0.0.0.0:3333/',
setupNodeEvents(on, config) {
// implement node event listeners here
addMatchImageSnapshotPlugin(on, config)
},
}
});

View file

@ -0,0 +1,4 @@
{
"account": "test",
"password": "test"
}

View file

@ -0,0 +1,26 @@
describe('Replayer visual match test', () => {
it('Teklogiks sessions on 3 and 20 seconds are same', () => {
cy.intercept('/api/account').as('getAccount')
cy.intercept('/mobs/*').as('getSession')
cy.visit('/', {
onBeforeLoad: function (window) {
window.localStorage.setItem('notesFeatureViewed', 'true');
}
})
cy.get(':nth-child(1) > .relative > .p-2').type(Cypress.env('account'))
cy.get(':nth-child(2) > .relative > .p-2').type(Cypress.env('password'))
cy.get('.h-10').click()
cy.wait('@getAccount')
cy.visit('3/session/7585361734083637?jumpto=5000&freeze=true')
cy.wait(1000)
cy.matchImageSnapshot('1st-breakpoint');
cy.visit('3/session/7585361734083637?jumpto=20000&freeze=true')
// adjusting because we have more messages to load
cy.wait(3000)
cy.matchImageSnapshot('2nd-breakpoint');
})
})

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -0,0 +1,7 @@
const {
addMatchImageSnapshotPlugin,
} = require('cypress-image-snapshot/plugin');
module.exports = (on, config) => {
addMatchImageSnapshotPlugin(on, config);
};

View file

@ -0,0 +1,45 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
addMatchImageSnapshotCommand({
failureThreshold: 0.03, // threshold for entire image
failureThresholdType: "percent", // percent of image or number of pixels
customDiffConfig: { threshold: 0.1 }, // threshold for each pixel
capture: "viewport" // capture viewport in screenshot
});

View file

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View file

@ -1,3 +1,33 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {
// "preset": "jest-puppeteer" preset: 'ts-jest',
} testEnvironment: 'node',
moduleNameMapper: {
'^Types/(.+)$': '<rootDir>/app/types/$1',
'^App/(.+)$': '<rootDir>/app/$1',
},
transform: {
'^.+\\.(ts|tsx)?$': ['ts-jest', { isolatedModules: true, diagnostics: { warnOnly: true } }],
'^.+\\.(js|jsx)$': 'babel-jest',
},
moduleDirectories: ['node_modules', 'app'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
};
//
// module.exports = {
// globals: {
// "ts-jest": {
// tsConfig: "tsconfig.json",
// diagnostics: true
// },
// NODE_ENV: "test"
// },
// moduleNameMapper: {
// "^Types/(.+)$": "<rootDir>/app/types/$1"
// },
// moduleDirectories: ["node_modules", 'app'],
// moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
// verbose: true
// };

View file

@ -15,7 +15,9 @@
"storybook": "start-storybook -p 6006", "storybook": "start-storybook -p 6006",
"flow": "flow", "flow": "flow",
"postinstall": "yarn gen:icons && yarn gen:colors", "postinstall": "yarn gen:icons && yarn gen:colors",
"build-storybook": "build-storybook" "build-storybook": "build-storybook",
"test": "jest",
"cy:open": "cypress open"
}, },
"dependencies": { "dependencies": {
"@floating-ui/react-dom-interactions": "^0.10.3", "@floating-ui/react-dom-interactions": "^0.10.3",
@ -85,6 +87,7 @@
"@babel/preset-react": "^7.17.12", "@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12", "@babel/preset-typescript": "^7.17.12",
"@babel/runtime": "^7.17.9", "@babel/runtime": "^7.17.9",
"@jest/globals": "^29.3.1",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"@openreplay/sourcemap-uploader": "^3.0.0", "@openreplay/sourcemap-uploader": "^3.0.0",
"@storybook/addon-actions": "^6.5.12", "@storybook/addon-actions": "^6.5.12",
@ -114,6 +117,8 @@
"country-data": "0.0.31", "country-data": "0.0.31",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"cssnano": "^5.0.12", "cssnano": "^5.0.12",
"cypress": "^12.3.0",
"cypress-image-snapshot": "^4.0.1",
"deasync-promise": "^1.0.1", "deasync-promise": "^1.0.1",
"deploy-aws-s3-cloudfront": "^3.6.0", "deploy-aws-s3-cloudfront": "^3.6.0",
"dotenv": "^6.2.0", "dotenv": "^6.2.0",
@ -124,6 +129,7 @@
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"flow-bin": "^0.115.0", "flow-bin": "^0.115.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"jest": "^29.3.1",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"minio": "^7.0.18", "minio": "^7.0.18",
"moment-locales-webpack-plugin": "^1.2.0", "moment-locales-webpack-plugin": "^1.2.0",
@ -141,6 +147,7 @@
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"svgo": "^2.8.0", "svgo": "^2.8.0",
"tailwindcss": "^3.1.4", "tailwindcss": "^3.1.4",
"ts-jest": "^29.0.5",
"ts-node": "^10.7.0", "ts-node": "^10.7.0",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"webpack": "^5.72.1", "webpack": "^5.72.1",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,175 @@
export const issues = [
{
issueId: '9158adad14bcb1e0db18384c778a18f5ef2',
sessionId: 8119081922378909,
timestamp: 1673887658753,
seqIndex: 26901,
payload: { Rate: 71, Duration: 20804 },
projectId: 2325,
type: 'cpu',
contextString: 'https://app.openreplay.com/5095/session/8118970199817356',
context: null,
icon: undefined,
key: 0,
name: undefined,
time: 300321,
},
{
issueId: '915b2c49d8e08176a84c0d8e58732995fb8',
sessionId: 8119081922378909,
timestamp: 1673888064761,
seqIndex: 74984,
payload: { Rate: 80, Duration: 9684 },
projectId: 2325,
type: 'cpu',
contextString: 'https://app.openreplay.com/5095/session/8118613089434832',
context: null,
icon: undefined,
key: 1,
name: undefined,
time: 706329,
},
];
export const events = [
{
time: 519,
label: undefined,
key: 0,
target: { path: undefined, label: undefined },
name: 'Location',
type: 'LOCATION',
sessionId: 8119081922378909,
messageId: 14,
timestamp: 1673887358951,
host: 'app.openreplay.com',
referrer: '',
domContentLoadedTime: 1637,
firstPaintTime: 1674,
firstContentfulPaintTime: 1674,
loadTime: 1870,
speedIndex: 1889,
visuallyComplete: 5110,
timeToInteractive: 5328,
domBuildingTime: 337,
path: '/3064/sessions',
baseReferrer: '',
responseTime: 2,
responseEnd: 1295,
ttfb: null,
query: '',
value: '/3064/sessions',
url: '/3064/sessions',
fcpTime: 1674,
},
{
time: 10133,
label:
'Inicio De Prova SESSIONS ASSIST DASHBOARDS FD Saved Search 0 Clear Search SESSIONS BOOKMARKS NOTES A',
key: 1,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
{
time: 12287,
label: 'Past 7 Days',
key: 2,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
{
time: 13146,
label: 'Search sessions using any captured event (click, input, page, error...)',
key: 3,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
{
time: 13777,
label:
'Inicio De Prova Add Project Inicio De Prova Nova Proposta SESSIONS ASSIST DASHBOARDS FD INTERACTIONS',
key: 4,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
{
time: 14440,
label: 'Nova Proposta',
key: 5,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
{
time: 14496,
label: undefined,
key: 6,
target: { path: undefined, label: undefined },
name: 'Location',
type: 'LOCATION',
sessionId: 8119081922378909,
messageId: 2038,
timestamp: 1673887372928,
host: 'app.openreplay.com',
referrer: '',
domContentLoadedTime: null,
firstPaintTime: null,
firstContentfulPaintTime: null,
loadTime: null,
speedIndex: null,
visuallyComplete: null,
timeToInteractive: null,
domBuildingTime: null,
path: '/5095/sessions',
baseReferrer: '',
responseTime: null,
responseEnd: null,
ttfb: null,
query: '',
value: '/5095/sessions',
url: '/5095/sessions',
fcpTime: null,
},
{
time: 15166,
label: 'Search sessions using any captured event (click, input, page, error...)',
key: 7,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
{
time: 15726,
label: 'Search sessions using any captured event (click, input, page, error...)',
key: 8,
target: { path: undefined, label: undefined },
type: 'INPUT',
name: 'Input',
value: 'click',
},
{
time: 17270,
label: 'Click',
key: 9,
target: { path: undefined, label: undefined },
type: 'CLICK',
name: 'Click',
targetContent: undefined,
count: undefined,
},
];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
import WebPlayer from 'App/player/web/WebPlayer';
import SimpleStore from 'App/player/common/SimpleStore'
let store = new SimpleStore({
...WebPlayer.INITIAL_STATE,
})
const player = new WebPlayer(store, session, false)

View file

@ -0,0 +1,32 @@
import { describe, expect, test } from '@jest/globals';
import Session from 'Types/Session';
import { Click, Location } from 'Types/Session/event';
import Issue from 'Types/Session/issue';
import { session } from './mocks/sessionResponse';
import { issues, events } from "./mocks/sessionData";
describe('Testing Session class', () => {
const sessionInfo = new Session(session.data);
test('checking type instances', () => {
expect(sessionInfo).toBeInstanceOf(Session);
expect(sessionInfo.issues[0]).toBeInstanceOf(Issue);
expect(sessionInfo.events[0]).toBeInstanceOf(Location);
expect(sessionInfo.events[1]).toBeInstanceOf(Click);
});
test('checking basic session info(id, userId, issues and events lengths to match)', () => {
expect(sessionInfo.sessionId).toBe('8119081922378909');
expect(sessionInfo.isMobile).toBe(false);
expect(sessionInfo.userNumericHash).toBe(55003039);
expect(sessionInfo.userId).toBe('fernando.dufour@pravaler.com.br');
expect(sessionInfo.issues.length).toBe(2);
expect(sessionInfo.notesWithEvents.length).toBe(362);
});
test('checking issue mapping', () => {
expect([...sessionInfo.issues]).toMatchObject(issues);
});
test('checking events mapping', () => {
expect([...sessionInfo.events.slice(0, 10)]).toMatchObject(events)
})
});