Skip to content

Commit 91dc166

Browse files
authored
Merge pull request #32 from LambdaTest/stage
Release 1.1.4
2 parents a21cbe2 + b72f30e commit 91dc166

File tree

9 files changed

+613
-164
lines changed

9 files changed

+613
-164
lines changed

commands/storybook.js

Lines changed: 134 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,154 @@
11
const { default: axios } = require('axios')
2-
const fs = require('fs');
2+
const fs = require('fs')
33
const { sendDoM } = require('./utils/dom')
44
const { validateStorybookUrl, validateStorybookDir } = require('./utils/validate')
55
const { defaultSmartUIConfig } = require('./utils/config')
66
const { skipStory } = require('./utils/story')
7-
const { createStaticServer } = require('./utils/server')
7+
const { getLastCommit } = require('./utils/git')
8+
const static = require('./utils/static')
9+
var { constants } = require('./utils/constants');
10+
const { shortPolling } = require('./utils/polling');
811

912
async function storybook(serve, options) {
10-
let server, url;
11-
let type = /^https?:\/\//.test(serve) ? 'url' : 'dir'
13+
let type = /^https?:\/\//.test(serve) ? 'url' : 'dir';
14+
let storybookConfig = options.config ? options.config : defaultSmartUIConfig.storybook;
15+
1216
if (type === 'url') {
1317
await validateStorybookUrl(serve);
14-
url = serve;
18+
let url = serve;
19+
20+
// Convert browsers and resolutions arrays to string
21+
let resolutions = [];
22+
storybookConfig.resolutions.forEach(element => {
23+
resolutions.push(element.join('x'));
24+
});
25+
storybookConfig.resolutions = (!resolutions.length) ? 'all' : resolutions.toString();
26+
storybookConfig.browsers = (!storybookConfig.browsers.length) ? 'all' : storybookConfig.browsers.map(x => x.toLowerCase()).toString();
27+
28+
// Get stories object from stories.json and add url corresponding to every story ID
29+
await axios.get(new URL('stories.json', url).href)
30+
.then(async function (response) {
31+
let stories = {}
32+
for (const [storyId, storyInfo] of Object.entries(response.data.stories)) {
33+
if (!skipStory(storyInfo.name, storybookConfig)) {
34+
stories[storyId] = {
35+
name: storyInfo.name,
36+
kind: storyInfo.kind,
37+
url: new URL('/iframe.html?id=' + storyId + '&viewMode=story', url).href
38+
}
39+
}
40+
}
41+
42+
if (Object.keys(stories).length === 0) {
43+
console.log('[smartui] Error: No stories found');
44+
process.exit(0);
45+
}
46+
console.log('[smartui] Stories found: ', Object.keys(stories).length);
47+
48+
// Capture DoM of every story and send it to renderer API
49+
await sendDoM(url, stories, storybookConfig, options);
50+
})
51+
.catch(function (error) {
52+
if (error.response) {
53+
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
54+
} else if (error.request) {
55+
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
56+
} else {
57+
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
58+
}
59+
});
1560
} else {
16-
await validateStorybookDir(serve);
17-
server = await createStaticServer(serve);
18-
url = `http://localhost:${server.address().port}`;
19-
}
61+
let dirPath = serve;
62+
await validateStorybookDir(dirPath);
2063

21-
// TODO: modularize this and separate check for file exists
22-
let storybookConfig = defaultSmartUIConfig.storybook;
23-
if (options.config) {
24-
try {
25-
storybookConfig = JSON.parse(fs.readFileSync(options.config)).storybook;
26-
} catch (error) {
27-
console.log('[smartui] Error: ', error.message);
28-
process.exit(1);
29-
}
64+
// Get storyIds to be rendered
65+
let storyIds = static.filterStories(dirPath, storybookConfig)
3066

31-
storybookConfig.browsers.forEach(element => {
32-
if (!(['chrome', 'safari', 'firefox'].includes(element))) {
33-
console.log('[smartui] Error: Invalid value for browser. Accepted browsers are chrome, safari and firefox');
34-
process.exit(0);
35-
}
36-
});
37-
// TODO: Sanity check resolutions
38-
}
67+
// Upload Storybook static
68+
await static.getSignedUrl(options)
69+
.then(async function (response) {
70+
let { url, uploadId } = response.data.data;
3971

40-
// Convert browsers and resolutions arrays to string
41-
let resolutions = [];
42-
storybookConfig.resolutions.forEach(element => {
43-
resolutions.push(element.join('x'));
44-
});
45-
storybookConfig.resolutions = (!resolutions.length) ? 'all' : resolutions.toString();
46-
storybookConfig.browsers = (!storybookConfig.browsers.length) ? 'all' : storybookConfig.browsers.toString();
72+
// Compress static build
73+
await static.compress(dirPath, uploadId)
74+
.then(function () {
75+
console.log(`[smartui] ${dirPath} compressed.`)
76+
})
77+
.catch(function (err) {
78+
console.log(`[smartui] Cannot compress ${dirPath}. Error: ${err.message}`);
79+
process.exit(0);
80+
});
4781

48-
// Get stories object from stories.json and add url corresponding to every story ID
49-
await axios.get(new URL('stories.json', url).href)
50-
.then(async function (response) {
51-
let stories = {}
52-
for (const [storyId, storyInfo] of Object.entries(response.data.stories)) {
53-
if (!skipStory(storyInfo.name, storybookConfig)) {
54-
stories[storyId] = {
55-
name: storyInfo.name,
56-
kind: storyInfo.kind,
57-
url: new URL('/iframe.html?id=' + storyId + '&viewMode=story', url).href
82+
// Upload to S3
83+
const zipData = fs.readFileSync('storybook-static.zip');
84+
console.log('[smartui] Upload in progress...')
85+
await axios.put(url, zipData, {
86+
headers: {
87+
'Content-Type': 'application/zip',
88+
'Content-Length': zipData.length
89+
}})
90+
.then(function (response) {
91+
console.log(`[smartui] ${dirPath} uploaded.`);
92+
fs.rmSync('storybook-static.zip');
93+
})
94+
.catch(function (error) {
95+
console.log(`[smartui] Cannot upload ${dirPath}. Error: ${err.message}`);
96+
fs.rmSync('storybook-static.zip');
97+
process.exit(0);
98+
});
99+
100+
// Prepare payload data
101+
let browsers = []
102+
let resolutions = []
103+
storybookConfig.browsers.forEach(element => {
104+
browsers.push(element.toLowerCase());
105+
});
106+
storybookConfig.resolutions.forEach(element => {
107+
resolutions.push({ width: element[0], height: element[1] });
108+
});
109+
let commit = await getLastCommit();
110+
let payload = {
111+
downloadURL: url.substring(url.search(/.com/)+5, url.search(/.zip/)+4),
112+
uploadId: uploadId,
113+
projectToken: process.env.PROJECT_TOKEN,
114+
storybookConfig: {
115+
browsers: browsers,
116+
resolutions: resolutions,
117+
storyIds: storyIds
118+
},
119+
git: {
120+
branch: commit.branch,
121+
commitId: commit.shortHash,
122+
commitAuthor: commit.author.name,
123+
commitMessage: commit.subject,
124+
githubURL: process.env.GITHUB_URL || '',
58125
}
59126
}
60-
}
61-
62-
if (Object.keys(stories).length === 0) {
63-
console.log('[smartui] Error: No stories found');
64-
process.exit(0);
65-
} else {
66-
for (const [storyId, storyInfo] of Object.entries(stories)) {
67-
console.log('[smartui] Story found: ' + storyInfo.name);
127+
128+
// Call static render API
129+
await axios.post(new URL(constants[options.env].STATIC_RENDER_PATH, constants[options.env].BASE_URL).href, payload)
130+
.then(async function (response) {
131+
console.log('[smartui] Build in progress...');
132+
await shortPolling(response.data.data.buildId, 0, options);
133+
})
134+
.catch(function (error) {
135+
if (error.response) {
136+
console.log('[smartui] Build failed: Error: ', error.response.data.error?.message);
137+
} else {
138+
console.log('[smartui] Build failed: Error: ', error.message);
139+
}
140+
});
141+
})
142+
.catch(function (error) {
143+
if (error.response) {
144+
console.log('[smartui] Error: ', error.response.data.error?.message);
145+
} else {
146+
console.log('[smartui] Error: ', error.message);
68147
}
69-
}
70-
// Capture DoM of every story and send it to renderer API
71-
await sendDoM(url, stories, storybookConfig, options);
72-
})
73-
.catch(function (error) {
74-
if (error.response) {
75-
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
76-
} else if (error.request) {
77-
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
78-
} else {
79-
console.log('[smartui] Cannot fetch stories. Error: ', error.message);
80-
}
81-
});
82-
83-
// Close static server
84-
if (server) server?.close();
85-
148+
process.exit(0);
149+
});
150+
151+
}
86152
};
87153

88154
module.exports = { storybook };

commands/utils/constants.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ constants.stage = {
66
BUILD_STATUS_URL: "https://stage-api.lambdatestinternal.com/storybook/status",
77
BASE_URL: "https://stage-api.lambdatestinternal.com",
88
SB_BUILD_VALIDATE_PATH: "/storybook/validate",
9-
CHECK_UPDATE_PATH: "storybook/packageinfo"
9+
CHECK_UPDATE_PATH: "storybook/packageinfo",
10+
GET_SIGNED_URL_PATH: "/storybook/url",
11+
STATIC_RENDER_PATH: "/storybook/staticrender"
1012
};
1113
constants.prod = {
1214
AUTH_URL: "https://api.lambdatest.com/storybook/auth",
1315
RENDER_API_URL: "https://api.lambdatest.com/storybook/render",
1416
BUILD_STATUS_URL: "https://api.lambdatest.com/storybook/status",
1517
BASE_URL: "https://api.lambdatest.com",
1618
SB_BUILD_VALIDATE_PATH: "/storybook/validate",
17-
CHECK_UPDATE_PATH: "storybook/packageinfo"
19+
CHECK_UPDATE_PATH: "storybook/packageinfo",
20+
GET_SIGNED_URL_PATH: "/storybook/url",
21+
STATIC_RENDER_PATH: "/storybook/staticrender"
1822
};
23+
constants.VALID_BROWSERS = ['chrome', 'safari', 'firefox'];
1924

2025
module.exports = { constants };

commands/utils/dom.js

Lines changed: 8 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@ const fs = require('fs');
33
const path = require('path')
44
const formData = require('form-data');
55
const { JSDOM } = require("jsdom");
6-
const Table = require('cli-table3');
76
var { constants } = require('./constants');
87
const { getLastCommit } = require('./git');
9-
10-
var INTERVAL = 2000
11-
const MAX_INTERVAL = 512000
8+
const { shortPolling } = require('./polling');
129

1310
async function sendDoM(storybookUrl, stories, storybookConfig, options) {
1411
const createBrowser = require('browserless')
@@ -53,19 +50,20 @@ async function sendDoM(storybookUrl, stories, storybookConfig, options) {
5350
await browser.close()
5451

5552
// Create form
56-
// let commit = await getLastCommit();
53+
let commit = await getLastCommit();
5754
const form = new formData();
5855
for (const [storyId, storyInfo] of Object.entries(stories)) {
5956
const file = fs.readFileSync('doms/' + storyId + '.html');
60-
form.append('files', file, storyInfo.kind + ': ' + storyInfo.name + '.html');
57+
filename = storyInfo.kind.replaceAll('/', '#') + ': ' + storyInfo.name;
58+
form.append('files', file, filename+'.html');
6159
}
6260
form.append('resolution', storybookConfig.resolutions);
6361
form.append('browser', storybookConfig.browsers);
6462
form.append('projectToken', process.env.PROJECT_TOKEN);
65-
// form.append('branch', commit.branch);
66-
// form.append('commitId', commit.shortHash);
67-
// form.append('commitAuthor', commit.author.name);
68-
// form.append('commitMessage', commit.subject);
63+
form.append('branch', commit.branch);
64+
form.append('commitId', commit.shortHash);
65+
form.append('commitAuthor', commit.author.name);
66+
form.append('commitMessage', commit.subject);
6967

7068
githubURL = process.env.GITHUB_URL
7169
if (githubURL) {
@@ -97,80 +95,6 @@ async function sendDoM(storybookUrl, stories, storybookConfig, options) {
9795
});
9896
};
9997

100-
async function shortPolling(buildId, retries = 0, options) {
101-
await axios.get(new URL('?buildId=' + buildId, constants[options.env].BUILD_STATUS_URL).href, {
102-
headers: {
103-
projectToken: process.env.PROJECT_TOKEN
104-
}})
105-
.then(function (response) {
106-
if (response.data) {
107-
if (response.data.buildStatus === 'completed') {
108-
console.log('[smartui] Build successful\n');
109-
console.log('[smartui] Build details:\n',
110-
'Build URL: ', response.data.buildURL, '\n',
111-
'Build Name: ', response.data.buildName, '\n',
112-
'Total Screenshots: ', response.data.totalScreenshots, '\n',
113-
'Approved: ', response.data.buildResults.approved, '\n',
114-
'Changes found: ', response.data.buildResults.changesFound, '\n'
115-
);
116-
117-
if (response.data.screenshots && response.data.screenshots.length > 0) {
118-
import('chalk').then((chalk) => {
119-
const table = new Table({
120-
head: [
121-
{content: chalk.default.white('Story'), hAlign: 'center'},
122-
{content: chalk.default.white('Mis-match %'), hAlign: 'center'},
123-
]
124-
});
125-
response.data.screenshots.forEach(screenshot => {
126-
let mismatch = screenshot.mismatchPercentage
127-
table.push([
128-
chalk.default.yellow(screenshot.storyName),
129-
mismatch > 0 ? chalk.default.red(mismatch) : chalk.default.green(mismatch)
130-
])
131-
});
132-
console.log(table.toString());
133-
})
134-
} else {
135-
if (response.data.baseline) {
136-
console.log('No comparisons run. This is a baseline build.');
137-
} else {
138-
console.log('No comparisons run. No screenshot in the current build has the corresponding screenshot in baseline build.');
139-
}
140-
}
141-
return;
142-
} else {
143-
if (response.data.screenshots && response.data.screenshots.length > 0) {
144-
// TODO: show Screenshots processed current/total
145-
console.log('[smartui] Screenshots compared: ', response.data.screenshots.length)
146-
}
147-
}
148-
}
149-
150-
// Double the INTERVAL, up to the maximum INTERVAL of 512 secs (so ~15 mins in total)
151-
INTERVAL = Math.min(INTERVAL * 2, MAX_INTERVAL);
152-
if (INTERVAL == MAX_INTERVAL) {
153-
console.log('[smartui] Please check the build status on LambdaTest SmartUI.');
154-
return;
155-
}
156-
157-
setTimeout(function () {
158-
shortPolling(buildId, 0, options)
159-
}, INTERVAL);
160-
})
161-
.catch(function (error) {
162-
if (retries >= 3) {
163-
console.log('[smartui] Error: Failed getting build status.', error.message);
164-
console.log('[smartui] Please check the build status on LambdaTest SmartUI.');
165-
return;
166-
}
167-
168-
setTimeout(function () {
169-
shortPolling(buildId, retries+1, options);
170-
}, 2000);
171-
});
172-
};
173-
17498
function getBase64(url) {
17599
return axios.get(url, {
176100
responseType: "text",

0 commit comments

Comments
 (0)