Skip to content

Commit

Permalink
feat(protractor): Support for testing cookies
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-molak committed Apr 11, 2019
1 parent 082adeb commit 15e043b
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 2 deletions.
3 changes: 3 additions & 0 deletions packages/protractor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@
"@integration/testing-tools": "2.0.1-alpha.47",
"@serenity-js/assertions": "2.0.1-alpha.47",
"@serenity-js/core": "2.0.1-alpha.47",
"@serenity-js/local-server": "2.0.1-alpha.47",
"@types/express": "^4.16.1",
"@types/html-minifier": "^3.5.2",
"@types/selenium-webdriver": "^3.0.14",
"chromedriver": "^2.45.0",
"express": "^4.16.4",
"html-minifier": "^3.5.21",
"protractor": "^5.4.2",
"selenium-webdriver": "^3.6.0",
Expand Down
208 changes: 208 additions & 0 deletions packages/protractor/spec/screenplay/questions/Cookie.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { certificates, expect } from '@integration/testing-tools';
import { Ensure, equals } from '@serenity-js/assertions';
import { Actor, Question, Transform } from '@serenity-js/core';
import { LocalServer, ManageALocalServer, StartLocalServer, StopLocalServer } from '@serenity-js/local-server';
import express = require('express');

import { protractor } from 'protractor';
import { BrowseTheWeb, DeleteCookies, Navigate } from '../../../src';
import { Cookie } from '../../../src/screenplay';

describe('Cookie', () => {

// a tiny express server, setting response cookies
const cookieCutterApp = express().
get('/cookie', (req: express.Request, res: express.Response) => {
res.cookie(req.query.name, req.query.value, {
path: '/cookie',
domain: req.query.domain,
httpOnly: !! req.query.httpOnly,
secure: !! req.query.secure,
expires: req.query.expires && new Date(req.query.expires),
}).status(200).send();
});

function cookieCutterURLFor(path: string): Question<Promise<string>> {
return Transform.the(LocalServer.url(), url => `${ url }${ path }`);
}

describe('over HTTP', () => {

// Fun fact: Before Cookie Monster ate his first cookie, he believed his name was Sid. You're welcome.
const Sid = Actor.named('Sid').whoCan(
BrowseTheWeb.using(protractor.browser),
ManageALocalServer.runningAHttpListener(cookieCutterApp),
);

beforeEach(() => Sid.attemptsTo(StartLocalServer.onRandomPort()));
afterEach(() => Sid.attemptsTo(StopLocalServer.ifRunning()));
afterEach(() => Sid.attemptsTo(DeleteCookies.all()));

describe('when working with the value', () => {

/** @test {Cookie} */
/** @test {Cookie#valueOf} */
it('allows the actor to retrieve it', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.valueOf('favourite'), equals('chocolate-chip')),
));

/** @test {Cookie} */
/** @test {Cookie#valueOf} */
it(`returns an undefined when it can't retrieve it`, () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.valueOf('not-so-favourite'), equals(undefined)),
));

/** @test {Cookie} */
it('provides a sensible description of the question being asked', () => {
expect(Cookie.valueOf('favourite').toString()).to.equal('the value of the "favourite" cookie');
});
});

describe('when working with the path', () => {

/** @test {Cookie} */
/** @test {Cookie#valueOf} */
it('allows the actor to retrieve it', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.pathOf('favourite'), equals('/cookie')),
));

/** @test {Cookie} */
/** @test {Cookie#pathOf} */
it(`returns an undefined when it can't retrieve it`, () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.pathOf('not-so-favourite'), equals(undefined)),
));

/** @test {Cookie} */
it('provides a sensible description of the question being asked', () => {
expect(Cookie.pathOf('favourite').toString()).to.equal('the path of the "favourite" cookie');
});
});

describe('when working with the domain', () => {

/** @test {Cookie} */
/** @test {Cookie#valueOf} */
it('allows the actor to retrieve it', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.domainOf('favourite'), equals('127.0.0.1')),
));

/** @test {Cookie} */
/** @test {Cookie#domainOf} */
it(`returns an undefined when it can't retrieve it`, () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.domainOf('not-so-favourite'), equals(undefined)),
));

/** @test {Cookie} */
it('provides a sensible description of the question being asked', () => {
expect(Cookie.domainOf('favourite').toString()).to.equal('the domain of the "favourite" cookie');
});
});

describe('when working with http-only cookies', () => {

/** @test {Cookie} */
/** @test {Cookie#isHTTPOnly} */
it('allows the actor to confirm that a cookie is http-only', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.isHTTPOnly('favourite'), equals(false)),

Navigate.to(cookieCutterURLFor('/cookie?name=second_choice&value=shortbread&httpOnly=true')),
Ensure.that(Cookie.isHTTPOnly('second_choice'), equals(true)),
));

/** @test {Cookie} */
/** @test {Cookie#isHTTPOnly} */
it(`returns an undefined when it can't retrieve it`, () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.isHTTPOnly('not-so-favourite'), equals(undefined)),
));

/** @test {Cookie} */
it('provides a sensible description of the question being asked', () => {
expect(Cookie.isHTTPOnly('favourite').toString()).to.equal('the HTTP-only status of the "favourite" cookie');
});
});

describe('when working with an expiry date', () => {

function tomorrow(): Date {
const now = new Date();
const nextDay = new Date(now);
nextDay.setDate(nextDay.getDate() + 1);

return nextDay;
}

const expectedExpiryDate = tomorrow();

/** @test {Cookie} */
/** @test {Cookie#expiryDateOf} */
it('allows the actor to retrieve it', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor(`/cookie?name=expiring&value=chocolate-chip&expires=${ expectedExpiryDate.toISOString() }`)),
Ensure.that(Transform.the(Cookie.expiryDateOf('expiring'), date => date.getDay()), equals(expectedExpiryDate.getDay())),
));

/** @test {Cookie} */
/** @test {Cookie#expiryDateOf} */
it(`returns an undefined when it can't retrieve it`, () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.expiryDateOf('not-so-favourite'), equals(undefined)),
));

/** @test {Cookie} */
it('provides a sensible description of the question being asked', () => {
expect(Cookie.expiryDateOf('favourite').toString()).to.equal('the expiry date of the "favourite" cookie');
});
});

});

describe('when working with secure cookies', () => {

const Sid = Actor.named('Secure Sid').whoCan(
BrowseTheWeb.using(protractor.browser),
ManageALocalServer.runningAHttpsListener(cookieCutterApp, {
cert: certificates.cert,
key: certificates.key,
requestCert: true,
rejectUnauthorized: false,
}),
);

beforeEach(() => Sid.attemptsTo(StartLocalServer.onRandomPort()));
afterEach(() => Sid.attemptsTo(StopLocalServer.ifRunning()));
afterEach(() => Sid.attemptsTo(DeleteCookies.all()));

/** @test {Cookie} */
/** @test {Cookie#isSecure} */
it('allows the actor to confirm that a cookie is not secure', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.isSecure('favourite'), equals(false)),
));

/** @test {Cookie} */
/** @test {Cookie#isSecure} */
it('allows the actor to confirm that a cookie is secure', () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip&secure=1')),
Ensure.that(Cookie.isSecure('favourite'), equals(true)),
));

/** @test {Cookie} */
/** @test {Cookie#isSecure} */
it(`returns an undefined when it can't retrieve it`, () => Sid.attemptsTo(
Navigate.to(cookieCutterURLFor('/cookie?name=favourite&value=chocolate-chip')),
Ensure.that(Cookie.isSecure('not-so-favourite'), equals(undefined)),
));

/** @test {Cookie} */
it('provides a sensible description of the question being asked', () => {
expect(Cookie.isSecure('favourite').toString()).to.equal('the "secure" status of the "favourite" cookie');
});
});
});
20 changes: 18 additions & 2 deletions packages/protractor/src/screenplay/abilities/BrowseTheWeb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Ability, LogicError, UsesAbilities } from '@serenity-js/core';
import { ActionSequence, ElementArrayFinder, ElementFinder, Locator, protractor, ProtractorBrowser } from 'protractor';
import { Navigation } from 'selenium-webdriver';
import { Navigation, Options } from 'selenium-webdriver';
import { promiseOf } from '../../promiseOf';

/**
Expand Down Expand Up @@ -97,12 +97,28 @@ export class BrowseTheWeb implements Ability {
* Interface for defining sequences of complex user interactions.
* Each sequence will not be executed until `perform` is called.
*
* @returns {ActionSequence}
* @returns {external:selenium-webdriver.ActionSequence}
*/
actions(): ActionSequence {
return this.browser.actions();
}

/**
* @desc
* Interface for managing browser and driver state.
*
* @returns {external:selenium-webdriver.Options}
*/
manage(): Options {
/*
this.browser.manage().deleteCookie();
this.browser.manage().deleteAllCookies();
return this.browser.manage().getCookie('asd');
*/

return this.browser.manage();
}

/**
* @desc
* Locates a single element identified by the locator
Expand Down
38 changes: 38 additions & 0 deletions packages/protractor/src/screenplay/interactions/AddCookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { promiseOf } from '../../promiseOf';
import { BrowseTheWeb } from '../abilities';

export class DeleteCookies {
static called(cookieName: Answerable<string>) {
return new DeleteCookieCalled(cookieName);
}

static all() {
return new DeletesAllCookies();
}
}

class DeleteCookieCalled implements Interaction {
constructor(private readonly name: Answerable<string>) {
}

performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
return actor.answer(this.name)
.then(name => BrowseTheWeb.as(actor).manage().deleteCookie(name));
}

toString(): string {
return formatted `#actor deletes the "${ this.name }" cookie`;
}
}

class DeletesAllCookies implements Interaction {
performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
return promiseOf(BrowseTheWeb.as(actor).manage().deleteAllCookies());
}

toString(): string {
return `#actor deletes all cookies`;
}
}
38 changes: 38 additions & 0 deletions packages/protractor/src/screenplay/interactions/DeleteCookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Answerable, AnswersQuestions, Interaction, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { promiseOf } from '../../promiseOf';
import { BrowseTheWeb } from '../abilities';

export class DeleteCookies {
static called(cookieName: Answerable<string>) {
return new DeleteCookieCalled(cookieName);
}

static all() {
return new DeletesAllCookies();
}
}

class DeleteCookieCalled implements Interaction {
constructor(private readonly name: Answerable<string>) {
}

performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
return actor.answer(this.name)
.then(name => BrowseTheWeb.as(actor).manage().deleteCookie(name));
}

toString(): string {
return formatted `#actor deletes the "${ this.name }" cookie`;
}
}

class DeletesAllCookies implements Interaction {
performAs(actor: UsesAbilities & AnswersQuestions): Promise<void> {
return promiseOf(BrowseTheWeb.as(actor).manage().deleteAllCookies());
}

toString(): string {
return `#actor deletes all cookies`;
}
}
1 change: 1 addition & 0 deletions packages/protractor/src/screenplay/interactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './Clear';
export * from './Click';
export * from './DeleteCookies';
export * from './DoubleClick';
export * from './Enter';
export { ExecuteScript, LastScriptExecution } from './execute-script';
Expand Down
53 changes: 53 additions & 0 deletions packages/protractor/src/screenplay/questions/Cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Answerable, AnswersQuestions, Question, Transform, UsesAbilities } from '@serenity-js/core';
import { IWebDriverOptionsCookie } from 'selenium-webdriver';
import { BrowseTheWeb } from '../abilities';

export class Cookie {
static valueOf(cookieName: Answerable<string>): Question<Promise<string>> {
return Transform.the(new CookieDetails(cookieName), details => details && details.value)
.as(`the value of the "${ cookieName }" cookie`);
}

static pathOf(cookieName: Answerable<string>): Question<Promise<string>> {
return Transform.the(new CookieDetails(cookieName), details => details && details.path)
.as(`the path of the "${ cookieName }" cookie`);
}

static domainOf(cookieName: string) {
return Transform.the(new CookieDetails(cookieName), details => details && details.domain)
.as(`the domain of the "${ cookieName }" cookie`);
}

static isHTTPOnly(cookieName: string) {
return Transform.the(new CookieDetails(cookieName), details => details && !! details.httpOnly)
.as(`the HTTP-only status of the "${ cookieName }" cookie`);
}

static isSecure(cookieName: string) {
return Transform.the(new CookieDetails(cookieName), details => details && !! details.secure)
.as(`the "secure" status of the "${ cookieName }" cookie`);
}

static expiryDateOf(cookieName: string) {
return Transform.the(new CookieDetails(cookieName), details => {
// expiry date coming from webdriver is expressed in seconds
return details && details.expiry && new Date(Number(details.expiry) * 1000);
})
.as(`the expiry date of the "${ cookieName }" cookie`);
}
}

class CookieDetails implements Question<Promise<IWebDriverOptionsCookie>> {
constructor(private readonly name: Answerable<string>) {
}

answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<IWebDriverOptionsCookie> {
return actor.answer(this.name)
.then(name => BrowseTheWeb.as(actor).manage().getCookie(name))
.then(details => !! details ? details : undefined);
}

toString() {
return `the details of the "${ this.name } cookie`;
}
}
Loading

0 comments on commit 15e043b

Please sign in to comment.