Skip to content

Commit d8e3ee2

Browse files
Pinank SolankiPinank Solanki
authored andcommitted
SmartUI Storybook package
0 parents  commit d8e3ee2

File tree

12 files changed

+4347
-0
lines changed

12 files changed

+4347
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# LambdaTest SmartUI CLI
2+
The @lambdatest/smartui package is LambdaTest's command-line interface (CLI) aimed to help you run your SmartUI tests on LambdaTest platform.
3+
4+
5+
# Installation
6+
7+
### Prerequisites
8+
1. Node version >=14.15.0 required
9+
```
10+
node --version
11+
```
12+
13+
2. Storybook version >=6.4 required. Also, add the following to your `.storybook/main.js`. You can read more about this here [Storybook Feature flags](https://storybook.js.org/docs/react/configure/overview#feature-flags)
14+
```js
15+
module.exports = {
16+
features: {
17+
buildStoriesJson: true
18+
}
19+
}
20+
```
21+
22+
### Install package using npm
23+
```bash
24+
npm install -g [email protected]:Lambdatest/smartui-storybook.git
25+
```
26+
27+
# Usage
28+
29+
## Step 1: Set SmartUI Project Token in environment variables
30+
31+
<b>For Linux/macOS:</b>
32+
33+
```
34+
export PROJECT_TOKEN="your-project-token"
35+
```
36+
37+
<b>For Windows:</b>
38+
39+
```
40+
set PROJECT_TOKEN="your-project-token"
41+
```
42+
43+
## Step 2: Create config file
44+
```bash
45+
smartui config create .smartui.json
46+
```
47+
48+
## Step 3: Execute tests
49+
Run the following command to run tests on your Storybook stories. Provide your storybook url, build name and config file (Default config used if no config file provided)
50+
51+
```bash
52+
smartui storybook http://localhost:6006 --buildname Build1 --config .smartui.json
53+
```
54+
55+
# About LambdaTest
56+
57+
[LambdaTest](https://www.lambdatest.com/) is a cloud based selenium grid infrastructure that can help you run automated cross browser compatibility tests on 2000+ different browser and operating system environments. LambdaTest supports all programming languages and frameworks that are supported with Selenium, and have easy integrations with all popular CI/CD platforms. It's a perfect solution to bring your [selenium automation testing](https://www.lambdatest.com/selenium-automation) to cloud based infrastructure that not only helps you increase your test coverage over multiple desktop and mobile browsers, but also allows you to cut down your test execution time by running tests on parallel.
58+
59+
# License
60+
61+
Licensed under the [MIT license](./LICENSE).

commands/config.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
const { defaultSmartUIConfig } = require('./utils/config')
4+
5+
function createConfig(filepath) {
6+
// default filepath
7+
filepath = filepath || '.smartui.json';
8+
let filetype = path.extname(filepath);
9+
if (filetype != '.json') {
10+
console.log(`[smartui] Error: Config file must have .json extension`);
11+
process.exit(1);
12+
}
13+
14+
// verify the file does not already exist
15+
if (fs.existsSync(filepath)) {
16+
console.log(`[smartui] Error: LambdaTest SmartUI config already exists: ${filepath}`);
17+
process.exit(1);
18+
}
19+
20+
// write stringified default config options to the filepath
21+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
22+
fs.writeFileSync(filepath, JSON.stringify(defaultSmartUIConfig, null, 2) + '\n');
23+
console.log(`[smartui] Created LambdaTest SmartUI config: ${filepath}`);
24+
};
25+
26+
module.exports = { createConfig };

commands/storybook.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const { default: axios } = require('axios')
2+
const fs = require('fs');
3+
const { sendDoM } = require('./utils/dom')
4+
const { validateStorybookUrl } = require('./utils/validate')
5+
const { defaultSmartUIConfig } = require('./utils/config')
6+
const { skipStory } = require('./utils/story')
7+
8+
async function storybook(url, options) {
9+
// Check connection with storybook url
10+
await validateStorybookUrl(url);
11+
12+
// TODO: modularize this and separate check for file exists
13+
let storybookConfig = defaultSmartUIConfig.storybook;
14+
if (options.config) {
15+
try {
16+
storybookConfig = JSON.parse(fs.readFileSync(options.config)).storybook;
17+
} catch (error) {
18+
console.log('[smartui] Error: ', error.message);
19+
process.exit(1);
20+
}
21+
22+
storybookConfig.browsers.forEach(element => {
23+
if (!(['chrome', 'safari', 'firefox'].includes(element))) {
24+
console.log('[smartui] Error: Invalid value for browser. Accepted browsers are chrome, safari and firefox');
25+
process.exit(0);
26+
}
27+
});
28+
// TODO: Sanity check resolutions
29+
}
30+
31+
// Convert browsers and resolutions arrays to string
32+
let resolutions = [];
33+
storybookConfig.resolutions.forEach(element => {
34+
resolutions.push(element.join('x'));
35+
});
36+
storybookConfig.resolutions = (!resolutions.length) ? 'all' : resolutions.toString();
37+
storybookConfig.browsers = (!storybookConfig.browsers.length) ? 'all' : storybookConfig.browsers.toString();
38+
39+
// Get stories object from stories.json and add url corresponding to every story ID
40+
axios.get(new URL('stories.json', url).href)
41+
.then(function (response) {
42+
let stories = {}
43+
for (const [storyId, storyInfo] of Object.entries(response.data.stories)) {
44+
if (!skipStory(storyInfo.name, storybookConfig)) {
45+
stories[storyId] = {
46+
name: storyInfo.name,
47+
kind: storyInfo.kind,
48+
url: new URL('/iframe.html?id=' + storyId + '&viewMode=story', url).href
49+
}
50+
}
51+
}
52+
53+
if (Object.keys(stories).length === 0) {
54+
console.log('[smartui] Error: No stories found');
55+
process.exit(0);
56+
} else {
57+
for (const [storyId, storyInfo] of Object.entries(stories)) {
58+
console.log('[smartui] Story found: ' + storyInfo.name);
59+
}
60+
}
61+
// Capture DoM of every story and send it to renderer API
62+
sendDoM(stories, storybookConfig, options);
63+
})
64+
.catch(function (error) {
65+
if (error.response) {
66+
console.log('[smartui] Error: Cannot fetch Storybook stories');
67+
} else if (error.request) {
68+
console.log('[smartui] Error: Cannot fetch Storybook stories');
69+
} else {
70+
console.log('[smartui] Error: Cannot fetch Storybook stories');
71+
}
72+
process.exit(0);
73+
});
74+
};
75+
76+
module.exports = { storybook };

commands/utils/config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const defaultSmartUIConfig = {
2+
storybook: {
3+
browsers: [
4+
'chrome',
5+
'firefox',
6+
'safari'
7+
],
8+
resolutions: [
9+
[1920, 1080]
10+
],
11+
include: [],
12+
exlcude: []
13+
}
14+
};
15+
16+
module.exports = { defaultSmartUIConfig }

commands/utils/constants.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
var constants = {}
2+
3+
constants.env = 'prod';
4+
constants.stage = {
5+
AUTH_URL: "https://stage-api.lambdatestinternal.com/storybook/auth",
6+
RENDER_API_URL: "https://stage-api.lambdatestinternal.com/storybook/render"
7+
};
8+
constants.prod = {
9+
AUTH_URL: "https://api.lambdatest.com/storybook/auth",
10+
RENDER_API_URL: "https://api.lambdatest.com/storybook/render"
11+
};
12+
13+
module.exports = { constants };

commands/utils/dom.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const axios = require('axios');
2+
const fs = require('fs');
3+
const formData = require('form-data');
4+
var { constants } = require('.//constants');
5+
6+
async function sendDoM(stories, storybookConfig, options) {
7+
const createBrowser = require('browserless')
8+
const browser = createBrowser()
9+
10+
if (!fs.existsSync('doms')){
11+
fs.mkdir('doms', (err) => {
12+
if (err) {
13+
return console.error(err);
14+
}
15+
});
16+
}
17+
for (const [storyId, storyInfo] of Object.entries(stories)) {
18+
const browserless = await browser.createContext()
19+
const html = await browserless.html(storyInfo.url)
20+
21+
try {
22+
fs.writeFileSync('doms/' + storyId + '.html', html);
23+
} catch (err) {
24+
console.error(err);
25+
}
26+
27+
await browserless.destroyContext()
28+
}
29+
await browser.close()
30+
31+
// Send html files to the renderer API
32+
const form = new formData();
33+
for (const [storyId, storyInfo] of Object.entries(stories)) {
34+
const file = fs.readFileSync('doms/' + storyId + '.html');
35+
form.append('html', file, storyInfo.kind + ': ' + storyInfo.name + '.html');
36+
}
37+
form.append('resolution', storybookConfig.resolutions);
38+
form.append('browser', storybookConfig.browsers);
39+
form.append('projectToken', process.env.PROJECT_TOKEN);
40+
form.append('buildName', options.buildname);
41+
axios.post(constants[constants.env].RENDER_API_URL, form, {
42+
headers: {
43+
...form.getHeaders()
44+
}
45+
})
46+
.then(function (response) {
47+
console.log('[smartui] Build successful')
48+
})
49+
.catch(function (error) {
50+
fs.rm('doms', {recursive: true}, (err) => {
51+
if (err) {
52+
return console.error(err);
53+
}
54+
});
55+
console.log('[smartui] Build failed: Error: ', error.message);
56+
process.exit(0);
57+
});
58+
59+
fs.rm('doms', {recursive: true}, (err) => {
60+
if (err) {
61+
return console.error(err);
62+
}
63+
});
64+
};
65+
66+
module.exports = { sendDoM };

commands/utils/story.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Returns true or false if the story should be skipped based on include and exclude config
2+
function skipStory(name, config) {
3+
let matches = regexp => {
4+
if (typeof regexp === 'string') {
5+
let [, parsed, flags] = /^\/(.+)\/(\w+)?$/.exec(regexp) || [];
6+
regexp = new RegExp(parsed ?? regexp, flags);
7+
}
8+
9+
return regexp?.test?.(name);
10+
};
11+
12+
let include = [].concat(config?.include).filter(Boolean);
13+
let exclude = [].concat(config?.exclude).filter(Boolean);
14+
15+
let skip = include?.length ? !include.some(matches) : false;
16+
if (!skip && !exclude?.some(matches)) return false;
17+
return true;
18+
};
19+
20+
module.exports = { skipStory };

commands/utils/validate.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const axios = require('axios');
2+
var { constants } = require('./constants');
3+
4+
function validateProjectToken() {
5+
if (process.env.PROJECT_TOKEN) {
6+
return axios.get(constants[constants.env].AUTH_URL, {
7+
headers: {
8+
projectToken: process.env.PROJECT_TOKEN
9+
}})
10+
.then(function (response) {
11+
console.log('[smartui] Project Token Validated');
12+
})
13+
.catch(function (error) {
14+
if (error.response) {
15+
console.log('[smartui] ] Error: Invalid Project Token');
16+
} else if (error.request) {
17+
console.log('[smartui] ] Error: Project Token could not be validated');
18+
} else {
19+
console.log('[smartui] ] Error: Project Token could not be validated');
20+
}
21+
process.exit(0);
22+
});
23+
}
24+
else {
25+
console.log('[smartui] Error: No PROJECT_TOKEN set');
26+
process.exit(0);
27+
}
28+
};
29+
30+
function validateStorybookUrl(url) {
31+
let aboutUrl;
32+
try {
33+
aboutUrl = new URL('?path=/settings/about', url).href;
34+
} catch (error) {
35+
console.log('[smartui] Error: ', error.message)
36+
process.exit(0);
37+
}
38+
return axios.get(aboutUrl)
39+
.then(function (response) {
40+
console.log('[smartui] Connection to storybook established');
41+
})
42+
.catch(function (error) {
43+
if (error.response) {
44+
console.log('[smartui] Error: Connection to storybook could not be established');
45+
} else if (error.request) {
46+
console.log('[smartui] Error: Connection to storybook could not be established');
47+
} else {
48+
console.log('[smartui] Error: Connection to storybook could not be established');
49+
}
50+
process.exit(0);
51+
});
52+
};
53+
54+
module.exports = { validateProjectToken, validateStorybookUrl };

index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#! /usr/bin/env node
2+
3+
const { Command } = require('commander');
4+
const program = new Command();
5+
const { storybook } = require('./commands/storybook');
6+
const { validateProjectToken } = require('./commands/utils/validate');
7+
const { createConfig } = require('./commands/config');
8+
9+
program
10+
.name('smartui')
11+
.description('CLI to help you run your SmartUI tests on LambdaTest platform')
12+
.version('1.0.0');
13+
14+
const configCommand = program.command('config')
15+
.description('Manage LambdaTest SmartUI config')
16+
17+
configCommand.command('create')
18+
.description('Create LambdaTest SmartUI config file')
19+
.argument('[filepath]', 'Optional config filepath')
20+
.action(function(filepath) {
21+
createConfig(filepath);
22+
});
23+
24+
program.command('storybook')
25+
.description('Snapshot Storybook stories')
26+
.argument('<url>', 'Storybook url')
27+
.requiredOption('--buildname <string>', 'Build name')
28+
.option('-c --config <file>', 'Config file path')
29+
.action(async function(url, options) {
30+
await validateProjectToken();
31+
storybook(url, options);
32+
});
33+
34+
program.parse();

0 commit comments

Comments
 (0)