diff --git a/package.json b/package.json index 36453769e6..91a0247968 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "eslint-plugin-import": "^2.20.2", "eslint-plugin-svelte3": "^2.7.3", "estree-walker": "^1.0.0", + "get-port": "^5.1.1", "is-reference": "^1.1.4", "jsdom": "^15.1.1", "kleur": "^3.0.3", @@ -85,10 +86,10 @@ "magic-string": "^0.25.3", "mocha": "^6.2.0", "periscopic": "^2.0.1", - "puppeteer": "^2.1.1", "rollup": "^1.27.14", "source-map": "^0.7.3", "source-map-support": "^0.5.13", + "testcafe": "^1.9.2", "tiny-glob": "^0.2.6", "tslib": "^1.10.0", "typescript": "^3.5.3" diff --git a/test/custom-elements/index.js b/test/custom-elements/index.js index 1329dbd2cf..76ba6b50b1 100644 --- a/test/custom-elements/index.js +++ b/test/custom-elements/index.js @@ -3,59 +3,113 @@ import * as path from 'path'; import * as http from 'http'; import { rollup } from 'rollup'; import * as virtual from '@rollup/plugin-virtual'; -import * as puppeteer from 'puppeteer'; + +import { TestcafeController, testcafeHolder } from '../testcafeController.js'; +import * as getPort from 'get-port'; +import { platform as os_platform } from 'os'; +import { execSync } from 'child_process'; + import { addLineNumbers, loadConfig, loadSvelte } from "../helpers.js"; import { deepEqual } from 'assert'; -const page = ` - -
- - +const pageHtml = `\ + + + + + Svelte Test + + +
+ + + `; const assert = fs.readFileSync(`${__dirname}/assert.js`, 'utf-8'); -describe('custom-elements', function() { - this.timeout(10000); +const testGroup = 'custom-elements'; +describe(testGroup, async function(env) { + this.timeout(20000); let svelte; let server; - let browser; + let serverPort; + let testcafe; + let controller; let code; function create_server() { return new Promise((fulfil, reject) => { const server = http.createServer((req, res) => { - if (req.url === '/') { - res.end(page); - } - - if (req.url === '/bundle.js') { - res.end(code); - } + if (req.url == '/') res.end(pageHtml); + if (req.url == '/bundle.js') res.end(code); }); - server.on('error', reject); - - server.listen('6789', () => { + server.listen(serverPort, () => { fulfil(server); }); }); } before(async () => { + this.timeout(10000); + svelte = loadSvelte(); - console.log('[custom-element] Loaded Svelte'); + console.log(' i Loaded Svelte'); + + serverPort = await getPort(); server = await create_server(); - console.log('[custom-element] Started server'); - browser = await puppeteer.launch(); - console.log('[custom-element] Launched puppeteer browser'); + console.log(` i Started server at http://localhost:${serverPort}/`); + + // init testcafe + + // write tempfile + TestcafeController.createTestFile({ + fixtureName: testGroup, + }); + + // guess browser + const testcafeBrowserList = await TestcafeController.testcafeListLocalBrowsers(); + // browsers sorted by personal taste + const preferredBrowsers = [ + 'chrome', 'chromium', 'chrome-canary', + 'firefox', 'safari', 'opera', 'edge', 'ie', + ]; + // get default browser name: harder on windows, crazy hard on darwin + // https://github.com/jakub-g/x-default-browser + const userDefaultBrowser = ['linux', 'freebsd'].includes(os_platform()) + ? execSync('xdg-mime query default x-scheme-handler/http').toString() + .replace(/.*(firefox|chrome|chromium|opera).*\n/i, '$1').toLowerCase() + : null; + const browser = testcafeBrowserList.includes(userDefaultBrowser) + ? userDefaultBrowser + : preferredBrowsers.find( // find first match + b => testcafeBrowserList.includes(b) + ) || testcafeBrowserList[0]; + const HL = browser.match(/(firefox|chro)/) ? ':headless' : ''; + + // find open ports + const port1 = await getPort(); + const port2 = await getPort(); + + console.log(' i Starting TestCafe in browser '+browser+HL); + testcafe = await TestcafeController.createTestCafeWithRunner( + [ browser+HL ], port1, port2, + ); + + console.log(' i Getting TestCafe controller'); + // this will print 'Running tests in: ....' + const testController = await testcafeHolder.get(); + controller = new TestcafeController(testController); + + console.log(' i init done'); }); after(async () => { if (server) server.close(); - if (browser) await browser.close(); + if (testcafe) await testcafe.close(); + TestcafeController.deleteTestFile(); }); fs.readdirSync(`${__dirname}/samples`).forEach(dir => { @@ -105,29 +159,60 @@ describe('custom-elements', function() { ] }); - const result = await bundle.generate({ format: 'iife', name: 'test' }); - code = result.output[0].code; + const generated = await bundle.generate({ format: 'iife', name: 'test' }); + code = generated.output[0].code; - const page = await browser.newPage(); + try { + //const t1 = (new Date()).getTime(); + + // start loading test page + await controller.t.navigateTo('http://localhost:'+serverPort+'/'); + + await controller.t.wait(50); // TODO better? + + // wait for page load: retry loop + // takes between 500 and 1500 ms + let testFunc; + for (let i = 0; i < 100; i++) { + try { + testFunc = controller.getFunc( + () => ( + test(document.querySelector('main')) + ) + ); + break; + } + catch (e) { + // script not yet loaded + // assert: e.errMsg == "ReferenceError: test is not defined" + await controller.t.wait(50); + process.stdout.write('.'); + } + } - page.on('console', (type, ...args) => { - console[type](...args); - }); + const testResult = await testFunc(); - page.on('error', error => { - console.log('>>> an error happened'); - console.error(error); - }); + //const t2 = (new Date()).getTime(); + //console.log(' '+(t2-t1)+' ms for page load'); - try { - await page.goto('http://localhost:6789'); + // print console messages + // in current version of testcafe + // the messages are not sorted by time (bug) + const testConsole = await controller.popConsole(); + ['error', 'log', 'info', 'warn'].forEach(key => { + if (testConsole[key].length > 0) { + console.log(key+': '+testConsole[key].join('\n'+key+': ')); + } + }) - const result = await page.evaluate(() => test(document.querySelector('main'))); - if (result) console.log(result); - } catch (err) { + if (testResult) console.log(testResult); + } + catch (err) { console.log(addLineNumbers(code)); - throw err; - } finally { + // testcafe error objects are verbose and useless + throw err.errMsg ? new Error(err.errMsg) : err; + } + finally { if (expected_warnings) { deepEqual(warnings.map(w => ({ code: w.code, diff --git a/test/testcafeController.js b/test/testcafeController.js new file mode 100644 index 0000000000..1bb7e7d015 --- /dev/null +++ b/test/testcafeController.js @@ -0,0 +1,163 @@ +// based on https://github.com/cvializ/testcafe-mocha + +import * as fs from 'fs'; +import * as createTestCafe from 'testcafe'; +import { ClientFunction, Selector, TestController } from 'testcafe'; +import * as browserProviderPool from 'testcafe/lib/browser/provider/pool'; + +export const testcafeHolder = { + testController: null, + captureResolver: null, + getResolver: null, + + capture: function(t) { + testcafeHolder.testController = t; + if (testcafeHolder.getResolver) { + testcafeHolder.getResolver(t); + } + return new Promise(function(resolve) { + testcafeHolder.captureResolver = resolve; + }); + }, + + free: function() { + testcafeHolder.testController = null; + if (testcafeHolder.captureResolver) { + testcafeHolder.captureResolver(); + } + }, + + get: function() { + return new Promise(function(resolve) { + if (testcafeHolder.testController) { + resolve(testcafeHolder.testController); + } else { + testcafeHolder.getResolver = resolve; + } + }); + }, +}; + +export class TestcafeController { + + // path is relative to project root + // must not contain slashes + static testcafeTempfile = 'temp_testcafeTestSrc.js'; + + static createTestCafeWithRunner(browserList, port1, port2) { + return createTestCafe('localhost', port1, port2) + .then(function(testcafe) { + const runner = testcafe.createRunner(); + // http://devexpress.github.io/testcafe/documentation/using-testcafe/programming-interface/runner.html + runner + .src('./'+TestcafeController.testcafeTempfile) + //.screenshots('reports/screenshots/', true) + .browsers(browserList) + .run(); + return testcafe; + }); + } + + // TODO better than tempfile? + // add method to testcafe.runner? + static createTestFile(options) { + const opt = Object.assign({}, { + // default options + fixtureName: 'fixture', + testName: 'test', + // import path is relative to project root + testcafeControllerFile: './test/testcafeController', + }, options); + fs.writeFileSync( + TestcafeController.testcafeTempfile, + ` + import { testcafeHolder } from ${JSON.stringify(opt.testcafeControllerFile)}; + fixture(${JSON.stringify(opt.fixtureName)}); + test(${JSON.stringify(opt.testName)}, testcafeHolder.capture); + ` + ); + } + + static deleteTestFile() { + fs.unlinkSync(TestcafeController.testcafeTempfile); + } + + static async testcafeListLocalBrowsers() { + // copy paste from testcafe/lib/cli/cli.js + const providerName = 'locally-installed'; + const provider = await browserProviderPool.getProvider(providerName); + if (provider && provider.isMultiBrowser) { + const browserNames = await provider.getBrowserList(); + await browserProviderPool.dispose(); + return browserNames; + } + else + return []; + } + + constructor(t) { + this.t = t; // testController + this._fn_getTitle = this.getFunc(() => document.title); + this.consolePos = null; + } + + bindFunc(fn) { + return fn.with({boundTestRun: this.t}); + } + + // run javascript in browser. sample use: + // const f = c.getFunc(() => document.title); + // const t = await f(); + getFunc(fn) { + return this.bindFunc(ClientFunction(fn)); + } + + // some utility functions + + async findFirstElement(cssSelector) { + return await this.bindFunc(Selector(cssSelector).nth(0)); + } + + // send keyboard string to browser + // handle null = document + async sendKeys(handle, keys) { + if (handle) + await this.t.typeText(handle.getElement(), keys); + else + await this.t.pressKey(keys); + } + + // get last console messages + async popConsole() { + const _console = await this.t.getBrowserConsoleMessages(); + + const res = this.consolePos + ? Object.keys(_console) + .reduce((acc, key) => { + acc[key] = _console[key].slice(this.consolePos[key]); + return acc; + }, {}) + : _console; + + // save array positions + this.consolePos = Object.keys(_console) + .reduce((acc, key) => { + acc[key] = _console[key].length; + return acc; + }, {}); + + return res; + } + + async getElementText(handle) { + return await handle.getElement().innerText; + } + + async getTitle() { + return await this._fn_getTitle(); + } + + async click(handle) { + return await this.t.click(handle.getElement()); + } +}