Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into PPLT_2182_add_type_check
Browse files Browse the repository at this point in the history
  • Loading branch information
chinmay-browserstack committed Sep 26, 2023
2 parents aa44223 + b17504f commit ea2d9cd
Show file tree
Hide file tree
Showing 8 changed files with 353 additions and 30 deletions.
65 changes: 65 additions & 0 deletions cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const { Undefined } = require('./validations');

class Cache {
static cache = {};

static capabilities = 'capabilities';
static sessionCapabilities = 'session_capabilites';
static commandExecutorUrl = 'command_executor_url';

static lastTime = Date.now();
static timeout = 5 * 60 * 1000;

static async withCache(store, key, func, cacheExceptions = false) {
this.maintain();
if (Undefined(this.cache[store])) this.cache[store] = {};

store = this.cache[store];
if (store[key]) {
if (store[key].success) {
return store[key].val;
} else {
throw store[key].val;
}
}

const obj = { success: false, val: null, time: Date.now() };
try {
obj.val = await func();
obj.success = true;
} catch (e) {
obj.val = e;
}

// We seem to have correct coverage for both flows but nyc is marking it as missing
// branch coverage anyway
/* istanbul ignore next */
if (obj.success || cacheExceptions) {
store[key] = obj;
}

if (!obj.success) throw obj.val;
return obj.val;
}

static maintain() {
if (this.lastTime + this.timeout > Date.now()) return;

for (const [, store] of Object.entries(this.cache)) {
for (const [key, item] of Object.entries(store)) {
if (item.time + this.timeout < Date.now()) {
delete store[key];
}
}
}
this.lastTime = Date.now();
}

static reset() {
this.cache = {};
}
}

module.exports = {
Cache
};
61 changes: 61 additions & 0 deletions driverMetadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// const utils = require('@percy/sdk-utils');
const { Cache } = require('./cache');
const { RequestInterceptor } = require('node-request-interceptor');
const withDefaultInterceptors = require('node-request-interceptor/lib/presets/default');

class DriverMetadata {
constructor(driver) {
this.driver = driver;
this.sessionId = null;
if (this.driver.constructor.name === 'Browser') {
this.type = 'wdio';
} else {
this.type = 'wd';
}
}

async getSessionId() {
if (!this.sessionId) {
if (this.type === 'wdio') this.sessionId = await this.driver.sessionId;
if (this.type === 'wd') this.sessionId = await (await this.driver.getSession()).getId();
}
return this.sessionId;
}

async getCapabilities() {
return await Cache.withCache(Cache.capabilities, await this.getSessionId(), async () => {
if (this.type === 'wdio') {
return await this.driver.capabilities;
} else {
const session = await this.driver.getSession();
const capabilities = Object.fromEntries(session.getCapabilities().map_);
return capabilities;
}
});
}

async getCommandExecutorUrl() {
return await Cache.withCache(Cache.commandExecutorUrl, await this.getSessionId(), async () => {
if (this.type === 'wdio') {
return `${this.driver.options.protocol}://${this.driver.options.hostname}${this.driver.options.path}`;
} else {
// To intercept request from driver. used to get remote server url
const interceptor = new RequestInterceptor(withDefaultInterceptors.default);
let commandExecutorUrl = '';
interceptor.use((req) => {
const url = req.url.href;
commandExecutorUrl = url.split('/session')[0];
});
// making a call so we can intercept commandExecutorUrl
await this.driver.getCurrentUrl();
// To stop intercepting request
interceptor.restore();
return commandExecutorUrl;
}
});
}
}

module.exports = {
DriverMetadata
};
32 changes: 5 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ const seleniumPkg = require('selenium-webdriver/package.json');
const CLIENT_INFO = `${sdkPkg.name}/${sdkPkg.version}`;
const ENV_INFO = `${seleniumPkg.name}/${seleniumPkg.version}`;
const utils = require('@percy/sdk-utils');
const { RequestInterceptor } = require('node-request-interceptor');
const withDefaultInterceptors = require('node-request-interceptor/lib/presets/default');
const { DriverMetadata } = require('./driverMetadata');

// Take a DOM snapshot and post it to the snapshot endpoint
module.exports = async function percySnapshot(driver, name, options) {
Expand Down Expand Up @@ -74,28 +73,7 @@ module.exports.percyScreenshot = async function percyScreenshot(driver, name, op
}

try {
let sessionId, capabilities, commandExecutorUrl;
if (driver.constructor.name === 'Browser') { // Logic for wdio
sessionId = driver.sessionId;
capabilities = driver.capabilities;
commandExecutorUrl = `${driver.options.protocol}://${driver.options.hostname}${driver.options.path}`;
} else { // Logic for selenium-webdriver
const session = await driver.getSession();
sessionId = session.getId();
capabilities = Object.fromEntries(session.getCapabilities().map_);

// To intercept request from driver. used to get remote server url
const interceptor = new RequestInterceptor(withDefaultInterceptors.default);
interceptor.use((req) => {
const url = req.url.href;
commandExecutorUrl = url.split('/session')[0];
});
// making a call so we can intercept commandExecutorUrl
await driver.getCurrentUrl();
// To stop intercepting request
interceptor.restore();
}

const driverData = new DriverMetadata(driver);
if (options) {
if ('ignoreRegionSeleniumElements' in options) {
options.ignore_region_selenium_elements = options.ignoreRegionSeleniumElements;
Expand All @@ -117,9 +95,9 @@ module.exports.percyScreenshot = async function percyScreenshot(driver, name, op
await module.exports.request({
environmentInfo: ENV_INFO,
clientInfo: CLIENT_INFO,
sessionId: sessionId,
commandExecutorUrl: commandExecutorUrl,
capabilities: capabilities,
sessionId: await driverData.getSessionId(),
commandExecutorUrl: await driverData.getCommandExecutorUrl(),
capabilities: await driverData.getCapabilities(),
snapshotName: name,
options: options

Check warning on line 102 in index.js

View workflow job for this annotation

GitHub Actions / Lint

Expected property shorthand
});
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@
"@percy/cli": "^1.27.1",
"@types/selenium-webdriver": "^4.0.9",
"cross-env": "^7.0.2",
"eslint": "^7.11.0",
"eslint-config-standard": "^16.0.1",
"eslint": "^8.27.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-n": "^15.5.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-standard": "^5.0.0",
"geckodriver": "^3.0.2",
"jasmine": "^4.4.0",
Expand Down
157 changes: 157 additions & 0 deletions test/cache.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Cache } from '../cache.js'

describe('Cache', () => {
const store = 'abc';
const key = 'key';

beforeEach(async () => {
Cache.reset();
});

describe('withCache', () => {
it('caches response', async () => {
const expectedVal = 123;
const func = jasmine.createSpy('func').and.returnValue(expectedVal);
let val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(1);
expect(val).toEqual(expectedVal);

val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(1);
expect(val).toEqual(expectedVal);
});

describe('with different key but same store', () => {
it('calls func again and caches it', async () => {
const expectedVal = 123;
const func = jasmine.createSpy('func').and.returnValue(expectedVal);
const key2 = 'key2';

let val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(1);
expect(val).toEqual(expectedVal);

val = await Cache.withCache(store, key2, func);
expect(func.calls.count()).toEqual(2);
expect(val).toEqual(expectedVal);

// test both cache
val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(2); // does not increment
expect(val).toEqual(expectedVal);

val = await Cache.withCache(store, key2, func);
expect(func.calls.count()).toEqual(2); // does not increment
expect(val).toEqual(expectedVal);
});
});

describe('with different store but same key', () => {
it('calls func again and caches it', async () => {
const expectedVal = 123;
const func = jasmine.createSpy('func').and.returnValue(expectedVal);
const store2 = 'store2';

let val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(1);
expect(val).toEqual(expectedVal);

val = await Cache.withCache(store2, key, func);
expect(func.calls.count()).toEqual(2);
expect(val).toEqual(expectedVal);

// test both cache
val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(2); // does not increment
expect(val).toEqual(expectedVal);

val = await Cache.withCache(store2, key, func);
expect(func.calls.count()).toEqual(2); // does not increment
expect(val).toEqual(expectedVal);
});
});

describe('with cacheExceptions', () => {
it('caches exceptions', async () => {
const expectedError = new Error('Some error');
const func = jasmine.createSpy('func').and.throwError(expectedError);

let actualError = null;
try {
await Cache.withCache(store, key, func, true);
} catch (e) {
actualError = e;
}

expect(func.calls.count()).toEqual(1);
expect(actualError).toEqual(expectedError);

try {
await Cache.withCache(store, key, func, true);
} catch (e) {
actualError = e;
}

expect(func.calls.count()).toEqual(1);
expect(actualError).toEqual(expectedError);
});
});

describe('with expired cache', () => {
const originalCacheTimeout = Cache.timeout;
beforeAll(() => {
Cache.timeout = 7; // 7ms
});

afterAll(() => {
Cache.timeout = originalCacheTimeout;
});

it('calls func again and caches it', async () => {
const expectedVal = 123;
const func = jasmine.createSpy('func').and.returnValue(expectedVal);

let val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(1);
expect(val).toEqual(expectedVal);

// wait for expiry
await new Promise((resolve) => setTimeout(resolve, 10));

// create a test entry that should not get deleted
Cache.cache.random_store = {};
Cache.cache.random_store.some_new_key = { val: 1, time: Date.now(), success: true };

// test expired cache
val = await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(2);
expect(val).toEqual(expectedVal);

// Not deleted
expect(Cache.cache.random_store.some_new_key).toBeTruthy();
});

it('it invalidates all expired keys on any call', async () => {
const expectedVal = 123;
const func = jasmine.createSpy('func').and.returnValue(expectedVal);
const key2 = 'key2';
const store2 = 'store2';

await Cache.withCache(store, key, func);
await Cache.withCache(store, key2, func);
await Cache.withCache(store2, key, func);

// wait for expiry
await new Promise((resolve) => setTimeout(resolve, 10));

// test expired cache
await Cache.withCache(store, key, func);
expect(func.calls.count()).toEqual(4);

// check internal to avoid calling via withCache
expect(Cache.cache[store2][key]).toBeUndefined();
expect(Cache.cache[store2][key2]).toBeUndefined();
});
});
});
});
Loading

0 comments on commit ea2d9cd

Please sign in to comment.