Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

codeceptjs

Package Overview
Dependencies
Maintainers
1
Versions
235
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

codeceptjs - npm Package Compare versions

Comparing version 1.3.1 to 1.3.2

11

CHANGELOG.md

@@ -0,1 +1,12 @@

## 1.3.2
* Interactve Shell improvements for `pause()`
* Added `next` command for **step-by-step debug** when using `pause()`.
* Use `After(pause);` in a to start interactive console after last step.
* [Puppeteer] Updated to Puppeteer 1.6.0
* Added `waitForRequest` to wait for network request.
* Added `waitForResponse` to wait for network response.
* Improved TypeScript definitions to support custom steps and page objects. By @xt1
* Fixed XPath detection to accept XPath which starts with `./` by @BenoitZugmeyer
## 1.3.1

@@ -2,0 +13,0 @@

12

docs/basics.md

@@ -61,4 +61,14 @@ # Basics

Interactive shell can be started outside test context by running
To **debug test step-by-step** type `next` and press Enter. The next step will be executed and interactive shell will be shown again.
To see all available commands press TAB two times to see list of all actions included in I.
If a test is failing you can prevent browser from closing by putting `pause()` command into `After()` hook. This is very helpful to debug failing tests. This way you can keep the same session and try different actions on a page to get the idea what went wrong.
```js
After(pause);
```
Interactive shell can be started outside the test context by running
```bash

@@ -65,0 +75,0 @@ codeceptjs shell

@@ -1226,12 +1226,27 @@ const requireg = require('requireg');

```js
I.waitForFunction(fn[, [args[, timeout]])
```
```js
I.waitForFunction(() => window.requests == 0);
I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
```
@param function to be executed in browser context
@param args arguments for function
@param sec time seconds to wait, 1 by default
*/
async waitForFunction(fn, sec = null) {
async waitForFunction(fn, argsOrSec = null, sec = null) {
let args = [];
if (argsOrSec) {
if (Array.isArray(argsOrSec)) {
args = argsOrSec;
} else if (typeof argsOrSec === 'number') {
sec = argsOrSec;
}
}
this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
return this.browser.wait(fn);
return this.browser.wait(fn, ...args);
}

@@ -1238,0 +1253,0 @@

3

docs/configuration.md

@@ -55,3 +55,4 @@ # Configuration

mocha: require('./mocha.conf.js') || {},
includes: {
include: {
I: './src/steps_file.js',
loginPage: './src/pages/login_page',

@@ -58,0 +59,0 @@ dashboardPage: new DashboardPage()

@@ -839,4 +839,9 @@ # Nightmare

```js
I.waitForFunction(fn[, [args[, timeout]])
```
```js
I.waitForFunction(() => window.requests == 0);
I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
```

@@ -847,3 +852,5 @@

- `function` to be executed in browser context
- `args` arguments for function
- `fn`
- `argsOrSec` (optional, default `null`)
- `sec` time seconds to wait, 1 by default

@@ -850,0 +857,0 @@

@@ -1184,4 +1184,9 @@ # Protractor

```js
I.waitForFunction(fn[, [args[, timeout]])
```
```js
I.waitForFunction(() => window.requests == 0);
I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
```

@@ -1192,5 +1197,6 @@

- `function` to be executed in browser context
- `args` arguments for function
- `fn`
- `argsOrSec` (optional, default `null`)
- `sec` time seconds to wait, 1 by default
- `timeoutMsg` (optional, default `null`)

@@ -1197,0 +1203,0 @@ ## waitForInvisible

@@ -1167,4 +1167,9 @@ # Puppeteer

```js
I.waitForFunction(fn[, [args[, timeout]])
```
```js
I.waitForFunction(() => window.requests == 0);
I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
```

@@ -1175,3 +1180,5 @@

- `function` to be executed in browser context
- `args` arguments for function
- `fn`
- `argsOrSec` (optional, default `null`)
- `sec` time seconds to wait, 1 by default

@@ -1201,2 +1208,30 @@

## waitForRequest
Waits for a network request.
```js
I.waitForRequest('http://example.com/resource');
I.waitForRequest(request => request.url() === 'http://example.com' && request.method() === 'GET');
```
**Parameters**
- `urlOrPredicate` **Any**
- `sec` **Any**
## waitForResponse
Waits for a network request.
```js
I.waitForResponse('http://example.com/resource');
I.waitForResponse(request => request.url() === 'http://example.com' && request.method() === 'GET');
```
**Parameters**
- `urlOrPredicate` **Any**
- `sec` **Any**
## waitForText

@@ -1203,0 +1238,0 @@

@@ -14,2 +14,6 @@ # WebDriverIO

- `browser`: browser in which to perform testing.
- `host`: (optional, default: localhost) - WebDriver host to connect.
- `port`: (optional, default: 4444) - WebDriver port to connect.
- `protocol`: (optional, default: http) - protocol for WebDriver server.
- `path`: (optional, default: /wd/hub) - path to WebDriver server,
- `restart`: (optional, default: true) - restart browser between tests.

@@ -1306,4 +1310,9 @@ - `smartWait`: (optional) **enables [SmartWait](http://codecept.io/acceptance/#smartwait)**; wait for additional milliseconds for element to appear. Enable for 5 secs: "smartWait": 5000.

```js
I.waitForFunction(fn[, [args[, timeout]])
```
```js
I.waitForFunction(() => window.requests == 0);
I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
```

@@ -1314,6 +1323,6 @@

- `function` to be executed in browser context
- `args` arguments for function
- `fn`
- `sec` time seconds to wait, 1 by default
Appium: support
- `timeoutMsg` (optional, default `null`)
- `argsOrSec` (optional, default `null`)
- `sec` time seconds to wait, 1 by defaultAppium: support

@@ -1320,0 +1329,0 @@ ## waitForInvisible

@@ -148,2 +148,30 @@ # Page Object

To use a Page Fragment within a Test Scenario just inject it into your Scenario:
```js
Scenario('failed_login', async (I, loginPage, modal) => {
loginPage.sendForm('john@doe.com','wrong password');
I.waitForVisible(modal.root);
within(modal.root, function () {
I.see('Login failed');
})
});
```
To use a Page Fragment within a Page Object, you need to `require` it on top of the Page Object file:
```js
const I = actor();
const modal = require('../fragments/modal');
module.exports = {
doStuff() {
...
modal.accept();
...
}
}
```
## StepObjects

@@ -150,0 +178,0 @@

@@ -12,19 +12,19 @@ const getConfig = require('./utils').getConfig;

const template = `
type ICodeceptCallback = (i: CodeceptJS.{{I}}) => void;
type ICodeceptCallback = (i: CodeceptJS.{{I}}{{callbackParams}}) => void;
declare class FeatureConfig {
retry(integer): FeatureConfig
timeout(integer): FeatureConfig
config(object): FeatureConfig
config(string, object): FeatureConfig
retry(times:number): FeatureConfig
timeout(seconds:number): FeatureConfig
config(config:object): FeatureConfig
config(helperName:string, config:object): FeatureConfig
}
declare class ScenarioConfig {
throws(any) : ScenarioConfig;
throws(err:any) : ScenarioConfig;
fails() : ScenarioConfig;
retry(integer): ScenarioConfig
timeout(integer): ScenarioConfig
inject(object): ScenarioConfig
config(object): ScenarioConfig
config(string, object): ScenarioConfig
retry(times:number): ScenarioConfig
timeout(timeout:number): ScenarioConfig
inject(inject:object): ScenarioConfig
config(config:object): ScenarioConfig
config(helperName:string, config:object): ScenarioConfig
}

@@ -43,23 +43,31 @@

declare class Locator implements ILocator {
or(locator): Locator;
find(locator): Locator;
withChild(locator): Locator;
find(locator): Locator;
at(position): Locator;
xpath?: string;
css?: string;
name?: string;
value?: string;
frame?: string;
android?: string;
ios?: string;
or(locator:string): Locator;
find(locator:string): Locator;
withChild(locator:string): Locator;
find(locator:string): Locator;
at(position:number): Locator;
first(): Locator;
last(): Locator;
inside(locator): Locator;
before(locator): Locator;
after(locator): Locator;
withText(text): Locator;
withAttr(attr): Locator;
as(output): Locator;
inside(locator:string): Locator;
before(locator:string): Locator;
after(locator:string): Locator;
withText(locator:string): Locator;
withAttr(locator:object): Locator;
as(locator:string): Locator;
}
declare function actor(customSteps?: {}): CodeceptJS.{{I}};
declare function Feature(string: string, opts?: {}): FeatureConfig;
declare function Scenario(string: string, callback: ICodeceptCallback): ScenarioConfig;
declare function Scenario(string: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
declare function xScenario(string: string, callback: ICodeceptCallback): ScenarioConfig;
declare function xScenario(string: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
declare function Feature(title: string, opts?: {}): FeatureConfig;
declare function Scenario(title: string, callback: ICodeceptCallback): ScenarioConfig;
declare function Scenario(title: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
declare function xScenario(title: string, callback: ICodeceptCallback): ScenarioConfig;
declare function xScenario(title: string, opts: {}, callback: ICodeceptCallback): ScenarioConfig;
declare function Data(data: any): any;

@@ -74,8 +82,9 @@ declare function xData(data: any): any;

declare function locate(selector: ILocator): Locator;
declare function within(selector: string, callback: Function): Promise;
declare function within(selector: ILocator, callback: Function): Promise;
declare function session(selector: string, callback: Function): Promise;
declare function session(selector: ILocator, callback: Function): Promise;
declare function session(selector: string, config: any, callback: Function): Promise;
declare function session(selector: ILocator, config: any, callback: Function): Promise;
declare function within(selector: string, callback: Function): Promise<any>;
declare function within(selector: ILocator, callback: Function): Promise<any>;
declare function session(selector: string, callback: Function): Promise<any>;
declare function session(selector: ILocator, callback: Function): Promise<any>;
declare function session(selector: string, config: any, callback: Function): Promise<any>;
declare function session(selector: ILocator, config: any, callback: Function): Promise<any>;
declare function pause(): void;

@@ -88,2 +97,3 @@ declare const codecept_helper: any;

}
{{exportPageObjects}}
}

@@ -96,2 +106,8 @@

const pageObjectTemplate = `
export interface {{name}} {
{{methods}}
}
`;
module.exports = function (genPath, options) {

@@ -117,20 +133,29 @@ const configFile = options.config || genPath;

const helper = helpers[name];
for (const action of methodsOfObject(helper)) {
const actionAlias = container.translation() ? container.translation().actionAliasFor(action) : action;
if (!actions[actionAlias]) {
methods = methods.concat(toTypeDef(helper[action]));
actions[actionAlias] = 1;
}
}
methods = addAllMethodsInObject(helper, actions, methods, translations);
}
for (const name in suppportI) {
if (actions[name]) {
continue;
}
const actor = suppportI[name];
// const params = toTypeDef(actor);
// methods.push(` ${(name)}: (${params}) => any; \n`);
methods = addAllNamesInObject(suppportI, actions, methods);
const supports = container.support(); // return all support objects
const exportPageObjects = [];
const callbackParams = [];
for (const name in supports) {
if (name === 'I') continue;
callbackParams.push(`${name}:any`);
const pageObject = supports[name];
const pageMethods = addAllMethodsInObject(pageObject, {}, []);
let pageObjectExport = pageObjectTemplate.replace('{{methods}}', pageMethods.join(''));
pageObjectExport = pageObjectExport.replace('{{name}}', name);
exportPageObjects.push(pageObjectExport);
}
let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, container.translation().I);
definitionsTemplate = definitionsTemplate.replace('{{exportPageObjects}}', exportPageObjects.join('\n'));
if (callbackParams.length > 0) {
definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${callbackParams.join(', ')}`);
} else {
definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', '');
}
if (translations) {
definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, translations.I);
}

@@ -146,1 +171,35 @@ fs.writeFileSync(path.join(testsPath, 'steps.d.ts'), definitionsTemplate);

};
function addAllMethodsInObject(supportObj, actions, methods, translations) {
for (const action of methodsOfObject(supportObj)) {
const actionAlias = translations ? translations.actionAliasFor(action) : action;
if (!actions[actionAlias]) {
methods.push(toTypeDef(supportObj[action]));
actions[actionAlias] = 1;
}
}
return methods;
}
function addAllNamesInObject(supportObj, actions, methods) {
for (const name in supportObj) {
if (actions[name]) {
continue;
}
const actor = supportObj[name];
let params = toTypeDef(actor);
if (params !== undefined) {
if (params.indexOf(' : ') > 0) {
if (params.indexOf(' (') > 0) {
params = params.trim();
methods.push(` ${(name)}${params}\n`);
} else {
methods.push(`${params}`);
}
} else {
methods.push(` ${(name)}: (${params}) => any; \n`);
}
}
}
return methods;
}

@@ -902,5 +902,13 @@ const requireg = require('requireg');

*/
async waitForFunction(fn, sec = null) {
async waitForFunction(fn, argsOrSec = null, sec = null) {
let args = [];
if (argsOrSec) {
if (Array.isArray(argsOrSec)) {
args = argsOrSec;
} else if (typeof argsOrSec === 'number') {
sec = argsOrSec;
}
}
this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
return this.browser.wait(fn);
return this.browser.wait(fn, ...args);
}

@@ -907,0 +915,0 @@

@@ -1384,5 +1384,14 @@ let EC;

*/
async waitForFunction(fn, sec = null, timeoutMsg = null) {
async waitForFunction(fn, argsOrSec = null, sec = null) {
let args = [];
if (argsOrSec) {
if (Array.isArray(argsOrSec)) {
args = argsOrSec;
} else if (typeof argsOrSec === 'number') {
sec = argsOrSec;
}
}
const aSec = sec || this.options.waitForTimeout;
return this.browser.wait(() => this.browser.executeScript.call(this.browser, fn), aSec * 1000, timeoutMsg);
return this.browser.wait(() => this.browser.executeScript.call(this.browser, fn, ...args), aSec * 1000);
}

@@ -1389,0 +1398,0 @@

@@ -170,3 +170,3 @@ const requireg = require('requireg');

} catch (e) {
return ['puppeteer@^1.5.0'];
return ['puppeteer@^1.6.0'];
}

@@ -509,3 +509,3 @@ }

// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
const { x, y } = await els[0]._visibleCenter();
const { x, y } = await els[0]._clickablePoint();
await this.page.mouse.move(x + offsetX, y + offsetY);

@@ -570,3 +570,4 @@ return this._waitForAction();

assertElementExists(els, locator, 'Element');
const elementCoordinates = await els[0]._visibleCenter();
await els[0]._scrollIntoViewIfNeeded();
const elementCoordinates = await els[0]._clickablePoint();
x = elementCoordinates.x;

@@ -1624,2 +1625,34 @@ y = elementCoordinates.y;

/**
* Waits for a network request.
*
* ```js
* I.waitForRequest('http://example.com/resource');
* I.waitForRequest(request => request.url() === 'http://example.com' && request.method() === 'GET');
* ```
*
* @param {*} urlOrPredicate
* @param {*} sec
*/
async waitForRequest(urlOrPredicate, sec = null) {
const timeout = sec ? sec * 1000 : this.options.waitForTimeout;
return this.page.waitForRequest(urlOrPredicate, { timeout });
}
/**
* Waits for a network request.
*
* ```js
* I.waitForResponse('http://example.com/resource');
* I.waitForResponse(request => request.url() === 'http://example.com' && request.method() === 'GET');
* ```
*
* @param {*} urlOrPredicate
* @param {*} sec
*/
async waitForResponse(urlOrPredicate, sec = null) {
const timeout = sec ? sec * 1000 : this.options.waitForTimeout;
return this.page.waitForResponse(urlOrPredicate, { timeout });
}
/**
* {{> ../webapi/switchTo }}

@@ -1671,7 +1704,14 @@ */

*/
async waitForFunction(fn, sec = null) {
const aSec = sec || this.options.waitForTimeout;
const waitTimeout = aSec * 1000;
async waitForFunction(fn, argsOrSec = null, sec = null) {
let args = [];
if (argsOrSec) {
if (Array.isArray(argsOrSec)) {
args = argsOrSec;
} else if (typeof argsOrSec === 'number') {
sec = argsOrSec;
}
}
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
const context = await this._getContext();
return context.waitForFunction(fn, { timeout: waitTimeout });
return context.waitForFunction(fn, { timeout: waitTimeout }, ...args);
}

@@ -1696,4 +1736,3 @@

console.log('This method will remove in CodeceptJS 1.4; use `waitForFunction` instead!');
const aSec = sec || this.options.waitForTimeout;
const waitTimeout = aSec * 1000;
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
const context = await this._getContext();

@@ -1899,5 +1938,5 @@ return context.waitForFunction(fn, { timeout: waitTimeout });

// Note: Using private api ._visibleCenter becaues the .BoundingBox does not take into account iframe offsets!
const dragSource = await src[0]._visibleCenter();
const dragDestination = await dst[0]._visibleCenter();
// Note: Using private api ._clickablePoint becaues the .BoundingBox does not take into account iframe offsets!
const dragSource = await src[0]._clickablePoint();
const dragDestination = await dst[0]._clickablePoint();

@@ -2035,2 +2074,1 @@ // Drag start point

}

@@ -251,3 +251,3 @@ const cssToXPath = require('css-to-xpath');

function isXPath(locator) {
return locator.substr(0, 2) === '//' || locator.substr(0, 3) === './/';
return locator.substr(0, 2) === '//' || locator.substr(0, 2) === './';
}

@@ -254,0 +254,0 @@

@@ -43,3 +43,9 @@ const parser = require('parse-function')({ ecmaVersion: 2017 });

if (fn.name.indexOf('grab') === 0) {
returnType = 'Promise';
if (fn.name.indexOf('BrowserLog') > 0 || fn.name.indexOf('ScrollPosition') > 0) {
returnType = 'Promise<object>';
}
if (fn.name.indexOf('Number') > 0) {
returnType = 'Promise<number>';
}
returnType = 'Promise<string>';
}

@@ -62,2 +68,5 @@ return ` ${fn.name}(${params}) : ${returnType},\n`;

case 'height':
case 'offset':
case 'offsetX':
case 'offsetY':
return ['number'];

@@ -64,0 +73,0 @@ }

const container = require('./container');
const recorder = require('./recorder');
const event = require('./event');
const output = require('./output');

@@ -12,2 +13,3 @@ const methodsOfObject = require('./utils').methodsOfObject;

let finish;
let next;

@@ -17,24 +19,40 @@ /**

*/
module.exports = function () {
recorder.add('Start new session', () => {
recorder.session.start('pause');
output.print(colors.yellow(' Interactive debug session started'));
output.print(colors.yellow(' Use JavaScript syntax to try steps in action'));
output.print(colors.yellow(` Press ${colors.bold('ENTER')} to continue`));
rl = readline.createInterface(process.stdin, process.stdout, completer);
rl.on('line', parseInput);
rl.on('close', () => {
console.log('Exiting interactive shell....');
const pause = function () {
next = false;
// add listener to all next steps to provide next() functionality
event.dispatcher.on(event.step.after, () => {
recorder.add('Start next pause session', () => {
if (!next) return;
return pauseSession();
});
return new Promise(((resolve) => {
finish = resolve;
return askForStep();
}));
});
recorder.add('Start new session', pauseSession);
};
function pauseSession() {
recorder.session.start('pause');
output.print(colors.yellow(' Interactive shell started'));
output.print(colors.yellow(` Press ${colors.bold('ENTER')} to resume test`));
if (!next) {
output.print(colors.yellow(' - Use JavaScript syntax to try steps in action'));
output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`));
output.print(colors.yellow(` - Enter ${colors.bold('next')} to run the next step`));
}
rl = readline.createInterface(process.stdin, process.stdout, completer);
rl.on('line', parseInput);
rl.on('close', () => {
console.log('Exiting interactive shell....');
});
return new Promise(((resolve) => {
finish = resolve;
return askForStep();
}));
}
function parseInput(cmd) {
rl.pause();
if (!cmd) {
next = false;
if (cmd === 'next') next = true;
if (!cmd || cmd === 'next') {
finish();

@@ -80,1 +98,3 @@ recorder.session.restore();

}
module.exports = pause;
{
"name": "codeceptjs",
"version": "1.3.1",
"version": "1.3.2",
"description": "Modern Era Acceptance Testing Framework for NodeJS",

@@ -40,5 +40,5 @@ "keywords": [

"chalk": "^1.1.3",
"commander": "^2.14.1",
"commander": "^2.16.0",
"css-to-xpath": "^0.1.0",
"cucumber-expressions": "^6.0.0",
"cucumber-expressions": "^6.0.1",
"escape-string-regexp": "^1.0.3",

@@ -51,5 +51,5 @@ "fn-args": "^3.0.0",

"mocha": "^4.1.0",
"parse-function": "^5.2.7",
"parse-function": "^5.2.10",
"promise-retry": "^1.1.1",
"requireg": "^0.1.5",
"requireg": "^0.1.8",
"sprintf-js": "^1.1.1"

@@ -59,12 +59,12 @@ },

"@types/inquirer": "^0.0.35",
"@types/node": "^8.10.12",
"@types/node": "^8.10.21",
"chai": "^3.4.1",
"chai-as-promised": "^5.2.0",
"co-mocha": "^1.2",
"documentation": "^4.0.0-beta1",
"eslint": "^4.17.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-import": "^2.13.0",
"faker": "^4.1.0",
"gulp": "^3.9.1",
"documentation": "^4.0.0-beta1",
"gulp-documentation": "^2.2.0",

@@ -74,11 +74,11 @@ "gulp-mustache": "^2.2.0",

"nightmare": "^3.0.0",
"nyc": "^11.5.0",
"protractor": "^5.3.0",
"puppeteer": "^1.5.0",
"nyc": "^11.9.0",
"protractor": "^5.3.2",
"puppeteer": "^1.6.0",
"rosie": "^1.6.0",
"sinon": "^1.17.2",
"sinon-chai": "^2.14.0",
"typescript": "^2.8.3",
"typescript": "^2.9.2",
"unirest": "^0.5.1",
"webdriverio": "^4.10.2",
"webdriverio": "^4.13.1",
"xmldom": "^0.1.27",

@@ -85,0 +85,0 @@ "xpath": "0.0.27"

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc