diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6471a1d..90fdc1e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,6 +34,7 @@ jobs: inputs: PathtoPublish: 'build' artifactName: build + - job: Test displayName: Tests pool: @@ -54,25 +55,6 @@ jobs: displayName: Flow - script: npm run test:node displayName: Unit tests - - job: Build_docs - displayName: Build docs - pool: - vmImage: 'Ubuntu-16.04' - steps: - - task: NodeTool@0 - inputs: - versionSpec: '10.x' - displayName: 'Install Node.js' - - task: Npm@0 - inputs: - command: install - - script: npm run build && cd www && npm install && npm run build && cd .. - displayName: Build docs - - task: PublishBuildArtifacts@1 - displayName: Upload docs website artifact - inputs: - PathtoPublish: 'www/public' - artifactName: docs - template: ci/browser-tests.yml parameters: @@ -138,3 +120,53 @@ jobs: displayName: Windows Internet Explorer 11 vmImage: 'vs2017-win2016' targetBrowser: IE_11 + + - job: Build_docs + displayName: Build docs + pool: + vmImage: 'Ubuntu-16.04' + dependsOn: + - Browser_Tests_Linux_Firefox_Stable + - Browser_Tests_Linux_Chrome_Stable + - Browser_Tests_OSX_Safari_IOS_9 + - Browser_Tests_OSX_Safari_IOS_10 + - Browser_Tests_OSX_Safari_IOS_11 + - Browser_Tests_OSX_Safari_Stable + - Browser_Tests_Windows_IE9 + - Browser_Tests_Windows_IE10 + - Browser_Tests_Windows_IE11 + steps: + - task: NodeTool@0 + inputs: + versionSpec: '10.x' + displayName: 'Install Node.js' + - task: Npm@0 + inputs: + command: install + - task: DownloadBuildArtifacts@0 + displayName: 'Download test results' + inputs: + artifactName: ReftestResults + downloadPath: $(System.DefaultWorkingDirectory) + - task: DownloadBuildArtifacts@0 + displayName: 'Download dist' + inputs: + artifactName: dist + downloadPath: $(System.DefaultWorkingDirectory) + - script: cp -R tests/reftests www/static/tests/reftests && cp -R tests/assets www/static/tests/assets && cp -R ReftestResults ./www/static/results + displayName: Copy reftests to docs website + - script: cp -R dist ./www/static/dist + displayName: Copy dist to docs website + - script: npm run build:reftest-result-list www/static/results/metadata www/src/results.json + displayName: Create reftest result index + - script: npm run build:reftest-preview + displayName: Create reftest previewer + - script: rm -rf www/static/results/metadata + displayName: Clean metadata folder + - script: npm run build && cd www && npm install && npm run build && cd .. + displayName: Build docs + - task: PublishBuildArtifacts@1 + displayName: Upload docs website artifact + inputs: + PathtoPublish: 'www/public' + artifactName: docs diff --git a/configs/base.json b/configs/base.json new file mode 100644 index 0000000..cfd1a02 --- /dev/null +++ b/configs/base.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "resolveJsonModule": true + } +} diff --git a/configs/preview.json b/configs/preview.json new file mode 100644 index 0000000..eb0ebe5 --- /dev/null +++ b/configs/preview.json @@ -0,0 +1,10 @@ +{ + "extends": "./base", + "include": [ + "../www/src/preview.ts" + ], + "exclude": [ + "node_modules" + ] +} + diff --git a/configs/scripts.json b/configs/scripts.json new file mode 100644 index 0000000..a922e86 --- /dev/null +++ b/configs/scripts.json @@ -0,0 +1,10 @@ +{ + "extends": "./base", + "include": [ + "scripts/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} + diff --git a/karma.js b/karma.js index 6a2b46f..acd06a9 100644 --- a/karma.js +++ b/karma.js @@ -16,9 +16,10 @@ const filenamifyUrl = require('filenamify-url'); const mkdirp = require('mkdirp'); const screenshotFolder = './tmp/reftests'; +const metadataFolder = './tmp/reftests/metadata'; mkdirp.sync(path.resolve(__dirname, screenshotFolder)); - +mkdirp.sync(path.resolve(__dirname, metadataFolder)); const CORS_PORT = 8081; const corsApp = express(); @@ -65,9 +66,10 @@ const writeScreenshot = (buffer, body) => { const filename = `${filenamifyUrl( body.test.replace(/^\/tests\/reftests\//, '').replace(/\.html$/, ''), {replacement: '-'} - )}!${[process.env.TARGET_BROWSER, body.platform.name, body.platform.version].join('-')}.png`; + )}!${[process.env.TARGET_BROWSER, body.platform.name, body.platform.version].join('-')}`; - fs.writeFileSync(path.resolve(__dirname, screenshotFolder, filename), buffer); + fs.writeFileSync(path.resolve(__dirname, screenshotFolder, `${filename}.png`), buffer); + return filename; }; app.post('/screenshot', (req, res) => { @@ -76,33 +78,16 @@ app.post('/screenshot', (req, res) => { } const buffer = new Buffer(req.body.screenshot.substring(prefix.length), 'base64'); - writeScreenshot(buffer, req.body); - return res.sendStatus(200); -}); - -const chunks = {}; - -app.post('/screenshot/chunk', (req, res) => { - if (!req.body || !req.body.screenshot) { - return res.sendStatus(400); - } - - const key = `${req.body.platform.name}-${req.body.platform.version}-${req.body.test - .replace(/^\/tests\/reftests\//, '') - .replace(/\.html$/, '')}`; - if (!Array.isArray(chunks[key])) { - chunks[key] = Array.from(Array(req.body.totalCount)); - } - - chunks[key][req.body.part] = req.body.screenshot; - - if (chunks[key].every(s => typeof s === 'string')) { - const str = chunks[key].reduce((acc, s) => acc + s, ''); - const buffer = new Buffer(str.substring(prefix.length), 'base64'); - delete chunks[key]; - writeScreenshot(buffer, req.body); - } - + const filename = writeScreenshot(buffer, req.body); + fs.writeFileSync(path.resolve(__dirname, metadataFolder, `${filename}.json`), JSON.stringify({ + windowWidth: req.body.windowWidth, + windowHeight: req.body.windowHeight, + platform: req.body.platform, + devicePixelRatio: req.body.devicePixelRatio, + test: req.body.test, + id: process.env.TARGET_BROWSER, + screenshot: filename + })); return res.sendStatus(200); }); diff --git a/package-lock.json b/package-lock.json index 061dfd5..5392bff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1499,6 +1499,12 @@ "integrity": "sha1-fyrX7FX5FEgvybHsS7GuYCjUYGY=", "dev": true }, + "@types/node": { + "version": "11.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.2.tgz", + "integrity": "sha512-HOtU5KqROKT7qX/itKHuTtt5fV0iXbheQvrgbLNXFJQBY/eh+VS5vmmTAVlo3qIGMsypm0G4N1t2AXjy1ZicaQ==", + "dev": true + }, "@types/rimraf": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz", @@ -2263,6 +2269,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -9474,6 +9486,12 @@ "pify": "^3.0.0" } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "mamacro": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", @@ -13393,6 +13411,40 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "ts-loader": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.3.3.tgz", + "integrity": "sha512-KwF1SplmOJepnoZ4eRIloH/zXL195F51skt7reEsS6jvDqzgc/YSbz9b8E07GxIUwLXdcD4ssrJu6v8CwaTafA==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^3.1.4", + "semver": "^5.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, + "ts-node": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.3.tgz", + "integrity": "sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -13452,6 +13504,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz", + "integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==", + "dev": true + }, "uglify-es": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", @@ -14561,6 +14619,12 @@ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", "dev": true }, + "yn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", + "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", + "dev": true + }, "zip-stream": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", diff --git a/package.json b/package.json index 2176f9e..02c2761 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://hertzen.com" }, "engines": { - "node": ">=4.0.0" + "node": ">=8.0.0" }, "repository": { "type": "git", @@ -26,6 +26,7 @@ "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", "@babel/preset-flow": "^7.0.0", + "@types/node": "^11.13.2", "appium-ios-simulator": "^3.10.0", "babel-eslint": "^10.0.1", "babel-loader": "^8.0.5", @@ -65,6 +66,9 @@ "serve-index": "^1.9.1", "slash": "1.0.0", "standard-version": "^5.0.2", + "ts-loader": "^5.3.3", + "ts-node": "^8.0.3", + "typescript": "^3.4.3", "uglifyjs-webpack-plugin": "^1.1.2", "webpack": "^4.29.6", "webpack-cli": "^3.3.0" @@ -73,6 +77,8 @@ "build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser", "build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js", "build:browser": "webpack", + "build:reftest-result-list": "ts-node scripts/create-reftest-result-list.ts", + "build:reftest-preview": "webpack --config www/webpack.config.js", "release": "standard-version", "rollup": "rollup -c", "format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www/src,tests,scripts}/**/*.js\"", diff --git a/scripts/create-reftest-result-list.ts b/scripts/create-reftest-result-list.ts new file mode 100644 index 0000000..469854c --- /dev/null +++ b/scripts/create-reftest-result-list.ts @@ -0,0 +1,44 @@ +import {readdirSync, readFileSync, writeFileSync} from 'fs'; +import {resolve} from 'path'; + +if (process.argv.length <= 2){ + console.log('No metadata path provided'); + process.exit(1); +} + +if (process.argv.length <= 3){ + console.log('No output file given'); + process.exit(1); +} + +const path = resolve(__dirname, '../', process.argv[2]); +const files = readdirSync(path); + +interface RefTestMetadata { + +} + +interface RefTestSingleMetadata extends RefTestMetadata{ + test: string; +} + +interface RefTestResults { + [key: string]: Array +} + +const result: RefTestResults = files.reduce((result: RefTestResults, file) => { + const json: RefTestSingleMetadata = JSON.parse(readFileSync(resolve(__dirname, path, file)).toString()); + if (!result[json.test]) { + result[json.test] = []; + } + + result[json.test].push(json); + delete json.test; + + return result; +}, {}); + +const output = resolve(__dirname, '../', process.argv[3]); +writeFileSync(output, JSON.stringify(result)); + +console.log(`Wrote file ${output}`); diff --git a/tests/testrunner.js b/tests/testrunner.js index 4a81a35..89f21b6 100644 --- a/tests/testrunner.js +++ b/tests/testrunner.js @@ -369,7 +369,10 @@ const assertPath = (result, expected, desc) => { platform: { name: platform.name, version: platform.version - } + }, + devicePixelRatio: window.devicePixelRatio || 1, + windowWidth: window.innerWidth, + windowHeight: window.innerHeight })); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cfd1a02 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "resolveJsonModule": true + } +} diff --git a/www/.gitignore b/www/.gitignore index 248335d..e78c007 100644 --- a/www/.gitignore +++ b/www/.gitignore @@ -6,3 +6,6 @@ node_modules public/ .DS_Store yarn-error.log +src/results.json +static/tests/preview.js +src/preview.js diff --git a/www/src/preview.ts b/www/src/preview.ts new file mode 100644 index 0000000..1e3447e --- /dev/null +++ b/www/src/preview.ts @@ -0,0 +1,115 @@ +import * as results from './results.json' ; +const testList: TestList = results; + +const testSelector: HTMLSelectElement | null = document.querySelector('#test_selector'); +const browserSelector: HTMLSelectElement | null = document.querySelector('#browser_selector'); +const previewImage: HTMLImageElement | null = document.querySelector('#preview_image'); +const testLink: HTMLAnchorElement | null = document.querySelector('#test_link'); + +interface Test { + windowWidth: number; + windowHeight: number; + platform: { + name: string; + version: string; + } + "devicePixelRatio": number; + "id": string; + "screenshot": string; +} + +type TestList = {[key: string]: Test[]}; + +function onTestChange(browserTests: Test[]) { + if (browserSelector) { + while (browserSelector.firstChild) { + browserSelector.firstChild.remove(); + } + browserTests.forEach((browser, i) => { + if (i === 0) { + onBrowserChange(browser); + } + const option = document.createElement('option'); + option.value = browser.id; + option.textContent = browser.id.replace(/_/g, ' '); + browserSelector.appendChild(option); + }); + } +} + +function onBrowserChange(browserTest: Test) { + if (previewImage) { + previewImage.src = `/results/${browserTest.screenshot}.png`; + if (browserTest.devicePixelRatio > 1) { + previewImage.style.transform = `scale(${1 / browserTest.devicePixelRatio})`; + } + } +} + +function selectTest(testName: string) { + if (testLink) { + testLink.textContent = testLink.href = testName; + } + onTestChange(testList[testName]); +} + +const UP_ARROW = 38; +const DOWN_ARROW = 40; +const LEFT_ARROW = 37; +const RIGHT_ARROW = 39; + +window.addEventListener('keydown', e => { + if (testSelector && browserSelector) { + if (e.keyCode === UP_ARROW) { + testSelector.selectedIndex = Math.max(0, testSelector.selectedIndex - 1); + const event = new Event('change'); + testSelector.dispatchEvent(event); + e.preventDefault(); + } else if (e.keyCode === DOWN_ARROW) { + testSelector.selectedIndex = Math.min(testSelector.children.length - 1, testSelector.selectedIndex + 1); + const event = new Event('change'); + testSelector.dispatchEvent(event); + e.preventDefault(); + } else if (e.keyCode === LEFT_ARROW) { + browserSelector.selectedIndex = Math.max(0, browserSelector.selectedIndex - 1); + const event = new Event('change'); + browserSelector.dispatchEvent(event); + e.preventDefault(); + } else if (e.keyCode === RIGHT_ARROW) { + browserSelector.selectedIndex = Math.min(browserSelector.children.length - 1, browserSelector.selectedIndex + 1); + const event = new Event('change'); + browserSelector.dispatchEvent(event); + e.preventDefault(); + } + } +}); + +if (testSelector && browserSelector) { + testSelector.addEventListener('change', () => { + selectTest(testSelector.value); + }, false); + + browserSelector.addEventListener('change', () => { + testList[testSelector.value].some(browser => { + if (browser.id === browserSelector.value) { + if (browser) { + onBrowserChange(browser); + } + return true; + } + return false; + }); + }, false); + + const tests: string[] = Object.keys(testList); + tests.forEach((testName, i) => { + if (i === 0) { + selectTest(testName); + } + const option = document.createElement('option'); + option.value = testName; + option.textContent = testName; + + testSelector.appendChild(option); + }); +} diff --git a/www/static/tests/index.html b/www/static/tests/index.html new file mode 100644 index 0000000..3e2b56e --- /dev/null +++ b/www/static/tests/index.html @@ -0,0 +1,20 @@ + + + + + html2canvas - Test result preview + + + +
+ + + Test link +
+
+ Preview image +
+ + + + diff --git a/www/webpack.config.js b/www/webpack.config.js new file mode 100644 index 0000000..5b72679 --- /dev/null +++ b/www/webpack.config.js @@ -0,0 +1,20 @@ +const path = require('path'); + +module.exports = { + mode: 'development', + target: 'web', + entry: path.resolve(__dirname, './src/preview.ts'), + output: { + path: path.resolve(__dirname, './static/tests'), + filename: 'preview.js' + }, + resolve: { + extensions: [".tsx", ".ts", ".js", ".json"] + }, + module: { + rules: [ + // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' + { test: /\.tsx?$/, use: ["ts-loader"], exclude: /node_modules/ } + ] + } +};