From 998742628f369739f3a7f065a4084392ef90973b Mon Sep 17 00:00:00 2001 From: Clemens Akens Date: Sun, 2 Apr 2017 22:10:56 +0200 Subject: [PATCH] feat: add `Browser.takeScreenshot ` action --- config-schema.json | 4 ++ fixtures/config.js | 3 +- package.json | 6 ++- src/__tests__/browser.test.ts | 84 ++++++++++++++++++++++++++++------- src/__tests__/element.test.ts | 32 +++++++------ src/__tests__/index.test.ts | 2 + src/browser.ts | 23 ++++++++++ src/config.ts | 4 +- src/index.ts | 3 +- typings/uuid.d.ts | 7 +++ 10 files changed, 133 insertions(+), 35 deletions(-) create mode 100644 typings/uuid.d.ts diff --git a/config-schema.json b/config-schema.json index dfdacae..cdf912e 100644 --- a/config-schema.json +++ b/config-schema.json @@ -61,6 +61,10 @@ "type": "number", "minimum": 0, "multipleOf": 1 + }, + "screenshotDirectory": { + "type": "string", + "minLength": 1 } }, "additionalProperties": false diff --git a/fixtures/config.js b/fixtures/config.js index 6b03fd2..57bbe68 100644 --- a/fixtures/config.js +++ b/fixtures/config.js @@ -5,5 +5,6 @@ module.exports = { exclude: ['custom-config.js'], include: '*.js', retries: 3, - retryDelay: 1000 + retryDelay: 1000, + screenshotDirectory: '/dev/null' }; diff --git a/package.json b/package.json index 9ec5283..f68692b 100644 --- a/package.json +++ b/package.json @@ -77,14 +77,18 @@ "watch:tests": "ava --fail-fast --watch" }, "dependencies": { + "@types/fs-extra": "0.0.37", + "@types/mz": "0.0.30", "@types/node": "7.0.12", "@types/proxyquire": "1.3.27", "ajv": "4.11.5", "deep-strict-equal": "0.2.0", + "fs-promise": "2.0.2", "glob": "7.1.1", "selenium-webdriver": "3.3.0", "tap": "10.3.1", - "tslib": "1.6.0" + "tslib": "1.6.0", + "uuid": "3.0.1" }, "devDependencies": { "ava": "0.18.2", diff --git a/src/__tests__/browser.test.ts b/src/__tests__/browser.test.ts index eb0e37d..d356388 100644 --- a/src/__tests__/browser.test.ts +++ b/src/__tests__/browser.test.ts @@ -1,19 +1,42 @@ // tslint:disable no-any +import proxyquire = require('proxyquire'); + import test from 'ava'; import {stub} from 'sinon'; -import {Browser} from '../browser'; import {format} from '../description'; import {Deferred} from './utils'; +const stubs = { + outputFile: stub(), + uuidV4: stub() +}; + +proxyquire('../browser', { + 'fs-promise': {outputFile: stubs.outputFile}, 'uuid/v4': stubs.uuidV4 +}); + +import {Browser} from '../browser'; + function createTestName(method: string, result: string): string { return `\`Browser.${method}\` should return an ${result}`; } +let browser: Browser; + +test.beforeEach(() => { + browser = new Browser('screenshotDirectory'); + + for (const key of Object.keys(stubs)) { + (stubs as any)[key].reset(); + (stubs as any)[key].resetBehavior(); + } +}); + test(createTestName('pageTitle', 'accessor'), async t => { t.plan(3); - const accessor = new Browser().pageTitle; + const accessor = browser.pageTitle; t.is(format(accessor.description), 'page title'); @@ -27,7 +50,7 @@ test(createTestName('pageTitle', 'accessor'), async t => { test(createTestName('pageUrl', 'accessor'), async t => { t.plan(3); - const accessor = new Browser().pageUrl; + const accessor = browser.pageUrl; t.is(format(accessor.description), 'page url'); @@ -41,7 +64,7 @@ test(createTestName('pageUrl', 'accessor'), async t => { test(createTestName('windowX', 'accessor'), async t => { t.plan(3); - const accessor = new Browser().windowX; + const accessor = browser.windowX; t.is(format(accessor.description), 'window x-position'); @@ -57,7 +80,7 @@ test(createTestName('windowX', 'accessor'), async t => { test(createTestName('windowY', 'accessor'), async t => { t.plan(3); - const accessor = new Browser().windowY; + const accessor = browser.windowY; t.is(format(accessor.description), 'window y-position'); @@ -73,7 +96,7 @@ test(createTestName('windowY', 'accessor'), async t => { test(createTestName('windowWidth', 'accessor'), async t => { t.plan(3); - const accessor = new Browser().windowWidth; + const accessor = browser.windowWidth; t.is(format(accessor.description), 'window width'); @@ -89,7 +112,7 @@ test(createTestName('windowWidth', 'accessor'), async t => { test(createTestName('windowHeight', 'accessor'), async t => { t.plan(3); - const accessor = new Browser().windowHeight; + const accessor = browser.windowHeight; t.is(format(accessor.description), 'window height'); @@ -106,7 +129,7 @@ test(createTestName('scriptResult', 'accessor'), async t => { t.plan(4); const script = () => undefined; - const accessor = new Browser().scriptResult('scriptName', script); + const accessor = browser.scriptResult('scriptName', script); t.is(format(accessor.description), 'result of script \'scriptName\''); @@ -122,7 +145,7 @@ test(createTestName('executeScript', 'action'), async t => { t.plan(4); const script = () => undefined; - const action = new Browser().executeScript('scriptName', script); + const action = browser.executeScript('scriptName', script); t.is(format(action.description), 'execute script \'scriptName\''); @@ -140,7 +163,7 @@ test(createTestName('executeScript', 'action'), async t => { test(createTestName('loadPage', 'action'), async t => { t.plan(4); - const action = new Browser().loadPage('pageUrl'); + const action = browser.loadPage('pageUrl'); t.is(format(action.description), 'load page \'pageUrl\''); @@ -158,7 +181,7 @@ test(createTestName('loadPage', 'action'), async t => { test(createTestName('maximizeWindow', 'action'), async t => { t.plan(3); - const action = new Browser().maximizeWindow(); + const action = browser.maximizeWindow(); t.is(format(action.description), 'maximize window'); @@ -175,7 +198,7 @@ test(createTestName('maximizeWindow', 'action'), async t => { test(createTestName('navigateBack', 'action'), async t => { t.plan(3); - const action = new Browser().navigateBack(); + const action = browser.navigateBack(); t.is(format(action.description), 'navigate back'); @@ -192,7 +215,7 @@ test(createTestName('navigateBack', 'action'), async t => { test(createTestName('navigateForward', 'action'), async t => { t.plan(3); - const action = new Browser().navigateForward(); + const action = browser.navigateForward(); t.is(format(action.description), 'navigate forward'); @@ -209,7 +232,7 @@ test(createTestName('navigateForward', 'action'), async t => { test(createTestName('reloadPage', 'action'), async t => { t.plan(3); - const action = new Browser().reloadPage(); + const action = browser.reloadPage(); t.is(format(action.description), 'reload page'); @@ -226,7 +249,7 @@ test(createTestName('reloadPage', 'action'), async t => { test(createTestName('setWindowPosition', 'action'), async t => { t.plan(5); - const action = new Browser().setWindowPosition(123, 456); + const action = browser.setWindowPosition(123, 456); t.is(format(action.description), 'set window position to 123,456'); @@ -247,7 +270,7 @@ test(createTestName('setWindowPosition', 'action'), async t => { test(createTestName('setWindowSize', 'action'), async t => { t.plan(5); - const action = new Browser().setWindowSize(123, 456); + const action = browser.setWindowSize(123, 456); t.is(format(action.description), 'set window size to 123x456'); @@ -266,7 +289,7 @@ test(createTestName('setWindowSize', 'action'), async t => { test(createTestName('sleep', 'action'), async t => { t.plan(2); - const action = new Browser().sleep(50); + const action = browser.sleep(50); t.is(format(action.description), `sleep for ${50} ms`); @@ -276,3 +299,30 @@ test(createTestName('sleep', 'action'), async t => { t.true(Date.now() - startTime >= 49); }); + +test(createTestName('takeScreenshot', 'action'), async t => { + t.plan(6); + + stubs.uuidV4.returns('uuid'); + + const action = browser.takeScreenshot(); + + t.is( + format(action.description), + 'take screenshot \'screenshotDirectory/uuid.png\'' + ); + + const takeScreenshot = stub().resolves('screenshot'); + const deferred = new Deferred(); + + stubs.outputFile.resolves(deferred); + + await action.perform({takeScreenshot} as any); + + t.true(deferred.done); + + t.is(stubs.outputFile.callCount, 1); + t.is(stubs.outputFile.args[0][0], 'screenshotDirectory/uuid.png'); + t.is(stubs.outputFile.args[0][1], 'screenshot'); + t.deepEqual(stubs.outputFile.args[0][2], {encoding: 'base64'}); +}); diff --git a/src/__tests__/element.test.ts b/src/__tests__/element.test.ts index 232a9ff..02a3a86 100644 --- a/src/__tests__/element.test.ts +++ b/src/__tests__/element.test.ts @@ -11,10 +11,16 @@ function createTestName(method: string, result: string): string { return `\`Element.${method}\` should return an ${result}`; } +let element: Element; + +test.beforeEach(() => { + element = new Element('selector'); +}); + test(createTestName('tagName', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').tagName; + const accessor = element.tagName; t.is(format(accessor.description), 'tag name of element \'selector\''); @@ -32,7 +38,7 @@ test(createTestName('tagName', 'accessor'), async t => { test(createTestName('text', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').text; + const accessor = element.text; t.is(format(accessor.description), 'text of element \'selector\''); @@ -50,7 +56,7 @@ test(createTestName('text', 'accessor'), async t => { test(createTestName('visibility', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').visibility; + const accessor = element.visibility; t.is(format(accessor.description), 'visibility of element \'selector\''); @@ -68,7 +74,7 @@ test(createTestName('visibility', 'accessor'), async t => { test(createTestName('x', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').x; + const accessor = element.x; t.is(format(accessor.description), 'x-position of element \'selector\''); @@ -86,7 +92,7 @@ test(createTestName('x', 'accessor'), async t => { test(createTestName('y', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').y; + const accessor = element.y; t.is(format(accessor.description), 'y-position of element \'selector\''); @@ -104,7 +110,7 @@ test(createTestName('y', 'accessor'), async t => { test(createTestName('width', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').width; + const accessor = element.width; t.is(format(accessor.description), 'width of element \'selector\''); @@ -122,7 +128,7 @@ test(createTestName('width', 'accessor'), async t => { test(createTestName('height', 'accessor'), async t => { t.plan(5); - const accessor = new Element('selector').height; + const accessor = element.height; t.is(format(accessor.description), 'height of element \'selector\''); @@ -140,7 +146,7 @@ test(createTestName('height', 'accessor'), async t => { test(createTestName('cssValue', 'accessor'), async t => { t.plan(6); - const accessor = new Element('selector').cssValue('cssName'); + const accessor = element.cssValue('cssName'); t.is( format(accessor.description), @@ -162,7 +168,7 @@ test(createTestName('cssValue', 'accessor'), async t => { test(createTestName('propertyValue', 'accessor'), async t => { t.plan(6); - const accessor = new Element('selector').propertyValue('propertyName'); + const accessor = element.propertyValue('propertyName'); t.is( format(accessor.description), @@ -184,7 +190,7 @@ test(createTestName('propertyValue', 'accessor'), async t => { test(createTestName('clearValue', 'action'), async t => { t.plan(5); - const action = new Element('selector').clearValue(); + const action = element.clearValue(); t.is(format(action.description), 'clear value of element \'selector\''); @@ -205,7 +211,7 @@ test(createTestName('clearValue', 'action'), async t => { test(createTestName('click', 'action'), async t => { t.plan(5); - const action = new Element('selector').click(); + const action = element.click(); t.is(format(action.description), 'click on element \'selector\''); @@ -226,7 +232,7 @@ test(createTestName('click', 'action'), async t => { test(createTestName('sendKeys', 'action'), async t => { t.plan(7); - const action = new Element('selector').sendKeys('key1', 'key2'); + const action = element.sendKeys('key1', 'key2'); t.is( format(action.description), @@ -252,7 +258,7 @@ test(createTestName('sendKeys', 'action'), async t => { test(createTestName('submitForm', 'action'), async t => { t.plan(5); - const action = new Element('selector').submitForm(); + const action = element.submitForm(); t.is( format(action.description), 'submit form containing element \'selector\'' diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 7147227..f63e5e3 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -17,6 +17,7 @@ Config: include: '*.js' retries: 3 retryDelay: 1000 + screenshotDirectory: '/dev/null' `; const defaultStderr = ` @@ -28,6 +29,7 @@ Config: include: '**/*.e2e.js' retries: 4 retryDelay: 500 + screenshotDirectory: 'screenshots' `; let customResult: Result; diff --git a/src/browser.ts b/src/browser.ts index 5571ac9..0006e20 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,3 +1,7 @@ +import uuidV4 = require('uuid/v4'); + +import {outputFile} from 'fs-promise'; +import {join} from 'path'; import {Accessor} from './accessor'; import {Action} from './action'; import {sleep} from './utils'; @@ -6,6 +10,12 @@ import {sleep} from './utils'; export type Script = (callback: (result?: any) => void) => void; export class Browser { + private readonly _screenshotDirectory: string; + + public constructor(screenshotDirectory: string) { + this._screenshotDirectory = screenshotDirectory; + } + public get pageTitle(): Accessor { return { description: {template: 'page title'}, @@ -120,4 +130,17 @@ export class Browser { perform: async () => sleep(duration) }; } + + public takeScreenshot(): Action { + const filename = join(this._screenshotDirectory, uuidV4() + '.png'); + + return { + description: {template: 'take screenshot {}', args: [filename]}, + perform: async driver => { + const screenshot = await driver.takeScreenshot(); + + await outputFile(filename, screenshot, {encoding: 'base64'}); + } + }; + } } diff --git a/src/config.ts b/src/config.ts index e6f9938..57ef94b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,6 +11,7 @@ export interface Config { readonly include: string; readonly retries: number; readonly retryDelay: number; + readonly screenshotDirectory: string; } const defaultConfig: Config = { @@ -20,7 +21,8 @@ const defaultConfig: Config = { exclude: ['**/node_modules/**/*'], include: '**/*.e2e.js', retries: 4, - retryDelay: 500 + retryDelay: 500, + screenshotDirectory: 'screenshots' }; const configFilename = process.argv[2]; diff --git a/src/index.ts b/src/index.ts index a8410eb..3c927c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,8 +39,7 @@ export class It { } export const it = new It(); - -export const browser = new Browser(); +export const browser = new Browser(config.screenshotDirectory); class TapTest extends Test { private readonly t: Tap.Test; diff --git a/typings/uuid.d.ts b/typings/uuid.d.ts new file mode 100644 index 0000000..a818724 --- /dev/null +++ b/typings/uuid.d.ts @@ -0,0 +1,7 @@ +// tslint:disable no-any + +declare module 'uuid/v4' { + function uuidV4(): string; + + export = uuidV4; +}