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());
+ }
+}