1 | const path = require('path');
|
2 | const fs = require('fs');
|
3 | const chalk = require('chalk');
|
4 | const {defaultsDeep, WdioPrepareConfig} = require('@aofl/cli-lib');
|
5 | const {LocalServer} = require('./modules/local-server');
|
6 | const findCacheDir = require('find-cache-dir');
|
7 | const Launcher = require('@wdio/cli').default;
|
8 | const {configMap} = require('./modules/wdio-config/wdio-presets');
|
9 | const spawn = require('cross-spawn');
|
10 | const rimraf = require('rimraf');
|
11 | const {titleCase} = require('title-case');
|
12 |
|
13 | class UnitTestingPlugin {
|
14 | static get name() {
|
15 | return 'AoflUnitTestingPlugin';
|
16 | }
|
17 |
|
18 | static get cacheDir() {
|
19 | return findCacheDir({name: UnitTestingPlugin.name, create: true});
|
20 | }
|
21 |
|
22 | static get nycOutputDir() {
|
23 | return path.join(UnitTestingPlugin.cacheDir, '.nyc_output');
|
24 | }
|
25 |
|
26 | constructor(options = {}) {
|
27 | this.options = defaultsDeep(options, {
|
28 | root: process.cwd(),
|
29 | output: '__build_tests',
|
30 | host: 'localhost',
|
31 | port: 3035,
|
32 | config: this.getConfigPath(),
|
33 | watch: false,
|
34 | debug: false,
|
35 | nycArgs: [
|
36 | 'report',
|
37 | '--reporter=lcov',
|
38 | '--reporter=text-summary',
|
39 | '--report-dir=./logs/coverage'
|
40 | ]
|
41 | });
|
42 |
|
43 | this.server = null;
|
44 | rimraf.sync(UnitTestingPlugin.cacheDir);
|
45 | }
|
46 |
|
47 | apply(compiler) {
|
48 | compiler.hooks.done.tapAsync(UnitTestingPlugin.name, async (stats, cb) => {
|
49 | try {
|
50 | const suites = [];
|
51 | for (let i = 0; i < this.options.entries.length; i++) {
|
52 | const entry = this.options.entries[i];
|
53 | if (stats.compilation.entrypoints.has(entry)) {
|
54 | suites.push({
|
55 | html: path.join(entry + '.html'),
|
56 | path: entry
|
57 | });
|
58 | }
|
59 | }
|
60 |
|
61 | if (this.server === null) {
|
62 | this.server = new LocalServer(
|
63 | path.resolve(this.options.output), this.options.host, this.options.port, this.options.debug
|
64 | );
|
65 | this.server.listen();
|
66 | }
|
67 |
|
68 | const wdioSuites = this.createSuites(suites);
|
69 | const wdioConfig = new WdioPrepareConfig(configMap, this.options.config, UnitTestingPlugin.name + '_config', this.options.debug);
|
70 | const wdioConfigPath = wdioConfig.generateConfig();
|
71 |
|
72 | const wdio = new Launcher(wdioConfigPath, {
|
73 | specs: wdioSuites,
|
74 | baseUrl: `http://${this.server.host}:${this.server.port}`
|
75 | });
|
76 |
|
77 | let code = await wdio.run();
|
78 | if (!this.options.watch) {
|
79 | this.server.close();
|
80 | if (this.options.nycArgs && code === 0) {
|
81 | code = await this.runNyc();
|
82 | }
|
83 | }
|
84 | if (code !== 0) {
|
85 | return cb(code);
|
86 | }
|
87 | } catch (e) {
|
88 | process.stdout.write(chalk.red('Launcher failed to start the test' + '\n'));
|
89 | return cb(e);
|
90 | }
|
91 | return cb(null);
|
92 | });
|
93 | }
|
94 |
|
95 | runNyc() {
|
96 | return new Promise((resolve, reject) => {
|
97 | const nyc = spawn('nyc', [
|
98 | ...this.options.nycArgs,
|
99 | `--temp-dir=${UnitTestingPlugin.nycOutputDir}`,
|
100 | '--clean'
|
101 | ], {
|
102 | stdio: 'pipe',
|
103 | env: {
|
104 | ...process.env,
|
105 | FORCE_COLOR: 1
|
106 | }
|
107 | });
|
108 |
|
109 | if (nyc.stdout !== null) {
|
110 | nyc.stdout.on('data', (data) => {
|
111 | process.stdout.write(data);
|
112 | });
|
113 | }
|
114 | nyc.on('close', (code, ...args) => {
|
115 | if (code === 0) {
|
116 | resolve(null);
|
117 | } else {
|
118 | reject(code);
|
119 | }
|
120 | });
|
121 | });
|
122 | }
|
123 | |
124 |
|
125 |
|
126 | getConfigPath() {
|
127 | const paths = [
|
128 | '.wct.config.js',
|
129 | '.wctrc.json',
|
130 | 'wct.conf.json'
|
131 | ];
|
132 | for (let i = 0; i < paths.length; i++) {
|
133 | try {
|
134 | const p = path.join(process.env.PWD, paths[i]);
|
135 | const stat = fs.statSync(p);
|
136 | if (stat.isFile()) {
|
137 | return p;
|
138 | }
|
139 | } catch (e) {
|
140 |
|
141 | }
|
142 | }
|
143 | }
|
144 |
|
145 | createSuites(suites) {
|
146 | const wdioSuites = [];
|
147 | for (let i = 0; i < suites.length; i++) {
|
148 | const suite = suites[i];
|
149 | const suiteName = titleCase(suite.path.replace(/\.spec\.js$/, ''));
|
150 | const target = path.join(UnitTestingPlugin.cacheDir, suite.path);
|
151 | const suiteContent = `const {expect} = require('chai');
|
152 | const logger = require('@wdio/logger').default;
|
153 | const chalk = require('chalk');
|
154 |
|
155 | const symbols = {
|
156 | ok: '✓',
|
157 | err: '✖',
|
158 | dot: '․',
|
159 | comma: ',',
|
160 | bang: '!'
|
161 | };
|
162 |
|
163 | const log = logger('@aofl/unit-testing-plugin');
|
164 | const getEnvironmentCombo = (caps) => {
|
165 | const device = caps.deviceName;
|
166 | const browser = (caps.browserName || caps.browser);
|
167 | const version = caps.version || caps.platformVersion || caps.browser_version;
|
168 | const platform = caps.os ? (caps.os + ' ' + caps.os_version) : (caps.platform || caps.platformName)
|
169 |
|
170 | // Mobile capabilities
|
171 | if (device) {
|
172 | const program = (caps.app || '').replace('sauce-storage:', '') || caps.browserName
|
173 | const executing = program ? 'executing ' + program : ''
|
174 |
|
175 | return ('device on ' + platform + ' ' + version + ' ' + executing).trim()
|
176 | }
|
177 |
|
178 | return browser + (version ? ' (v' + version + ')' : '') + (platform ? ' on ' + platform : '')
|
179 | }
|
180 |
|
181 | describe('${suiteName}', () => {
|
182 | before(function() {
|
183 | if (driver.isMobile === false) {
|
184 | try {
|
185 | browser.maximizeWindow();
|
186 | } catch (e) {}
|
187 | }
|
188 | });
|
189 |
|
190 | it('Running Tests /${suite.html}', function() {
|
191 | browser.url('/${suite.html}');
|
192 | browser.waitUntil(() => {
|
193 | const ready = browser.execute('return !(typeof window.aofljsConfig === "undefined" || typeof window.aofljsConfig.unitTesting === "undefined" || window.aofljsConfig.unitTesting.ready === false)');
|
194 | return ready === true;
|
195 | }, browser.config.waitforTimeout, 'expected tests to be ready in timeout period', browser.config.waitforInterval);
|
196 |
|
197 | let report = null;
|
198 | browser.waitUntil(() => {
|
199 | const r = browser.execute(function() {
|
200 | var report = {
|
201 | status: 'init',
|
202 | pass: true,
|
203 | stream: []
|
204 | };
|
205 |
|
206 | if (typeof aofljsConfig !== 'undefined' && typeof aofljsConfig.report !== 'undefined' && typeof aofljsConfig.report.status !== 'undefined' && typeof aofljsConfig.report.pass !== 'undefined' && typeof aofljsConfig.report.stream !== 'undefined') {
|
207 | var report = {
|
208 | status: aofljsConfig.report.status,
|
209 | pass: aofljsConfig.report.pass,
|
210 | stream: aofljsConfig.report.stream
|
211 | };
|
212 | aofljsConfig.report.stream = [];
|
213 | }
|
214 | return report;
|
215 | });
|
216 | if (typeof r === 'undefined') return false;
|
217 | if (report === null) {
|
218 | report = r;
|
219 | } else {
|
220 | report = {
|
221 | ...r,
|
222 | stream: report.stream.concat(r.stream)
|
223 | }
|
224 | }
|
225 |
|
226 | for (let i = 0; i < r.stream.length; i++) {
|
227 | const test = r.stream[i];
|
228 | const result = test[0];
|
229 | const testInfo = test[1];
|
230 | if (result === 'pass') {
|
231 | if (['warn', 'error', 'silent'].indexOf(browser.config.logLevel) === -1) {
|
232 | let out = '\\t' + chalk.green(symbols.ok) + ' ' + getEnvironmentCombo(browser.capabilities) + ' >> ' + testInfo.fullTitle + ' (' + testInfo.duration + 'ms)';
|
233 | if (testInfo.currentRetry > 0) {
|
234 | out += ' (Retry: ' + testInfo.currentRetry + ')';
|
235 | }
|
236 | process.stdout.write(out + '\\n');
|
237 | }
|
238 | log.info('pass - ' + testInfo.fullTitle);
|
239 | } else {
|
240 | if (browser.config.logLevel !== 'silent') {
|
241 | let out = '\t' + chalk.red(symbols.err) + ' ' + getEnvironmentCombo(browser.capabilities) + ' >> ' + testInfo.fullTitle + ' (' + testInfo.duration + 'ms)';
|
242 | if (testInfo.currentRetry > 0) {
|
243 | out += ' (Retry: ' + testInfo.currentRetry + ')';
|
244 | }
|
245 | process.stdout.write(out + '\\n');
|
246 | process.stdout.write('\\n\\t\\t' + chalk.red(testInfo.stack) + '\\n\\n');
|
247 | }
|
248 | log.error('fail - ' + testInfo.fullTitle + ' - error: ' + testInfo.err);
|
249 | log.error(testInfo.stack);
|
250 | }
|
251 | }
|
252 |
|
253 | return report.status === 'done';
|
254 | }, browser.config.waitforTimeout, 'expected tests to run within timeout period.', browser.config.waitforInterval);
|
255 |
|
256 | let coverage = {};
|
257 | let coverageIndex = 0;
|
258 | browser.waitUntil(() => {
|
259 | const c = browser.execute(function(index) {
|
260 | var min = index * 5;
|
261 | var max = (index + 1) * 5;
|
262 | var curr = 0;
|
263 | var data = [];
|
264 | for (var key in aofljsConfig.report.coverage) {
|
265 | if (curr >= min && curr < max) {
|
266 | data.push({
|
267 | key: key,
|
268 | value: aofljsConfig.report.coverage[key]
|
269 | });
|
270 | }
|
271 | curr++;
|
272 | }
|
273 | return {
|
274 | data: data,
|
275 | done: max > (Object.keys(aofljsConfig.report.coverage).length - 1)
|
276 | }
|
277 | }, coverageIndex++);
|
278 |
|
279 | for (let i = 0; i < c.data.length; i++) {
|
280 | coverage[c.data[i].key] = c.data[i].value;
|
281 | }
|
282 |
|
283 | return c.done;
|
284 | }, browser.config.waitforTimeout, 'Failed te retrieve coverage data.', 100);
|
285 | global.aoflUnitTesting.coverage.push(coverage);
|
286 | expect(report.pass).to.be.true;
|
287 | });
|
288 | });
|
289 | `;
|
290 | fs.mkdirSync(path.dirname(target), {recursive: true});
|
291 | fs.writeFileSync(target, suiteContent, {encoding: 'utf-8'});
|
292 | wdioSuites.push(target);
|
293 | }
|
294 | return wdioSuites;
|
295 | }
|
296 | }
|
297 |
|
298 | module.exports = UnitTestingPlugin;
|