Skip to content

Commit

Permalink
feat(testcontroller): add new properties to TestController typing (#113)
Browse files Browse the repository at this point in the history
* feat(testcontroller): add new properties to TestController typing

No actual code changes for TestController:
- Added the property testRun which itself has
properties.
- Added the test property that gives access to name, meta and fixture name.
- Added
fixtureCtx property.
- Added many examples of hook usage (global hooks and tagg hooks)

Co-authored-by: Arthur Warnier <arthur.warnier@gmail.com>
  • Loading branch information
Arthy000 and Arthy000 authored Jan 17, 2023
1 parent 3575c2e commit 293dd77
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 20 deletions.
47 changes: 27 additions & 20 deletions examples/google.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Given, When, Then, Before } from '@cucumber/cucumber';
import { Selector as NativeSelector } from 'testcafe';
import { Selector as NativeSelector } from '../src'; // use 'gherkin-testcafe' outside of this repository

const Selector = (input, t) => {
return NativeSelector(input).with({ boundTestRun: t });
Expand All @@ -9,10 +9,14 @@ const Selector = (input, t) => {
// except the full path which might be even more unstable
const privacyModale = '#xe7COe';

/* HOOKS */

Before('@googleHook', async (t: TestController) => {
t.ctx.hookValue = 'GitHub';
});

/* PREREQUISITES */

Given("I opened Google's search page", async (t: TestController) => {
await t.navigateTo('https://www.google.com');
});
Expand All @@ -21,21 +25,9 @@ Given(/^I dismissed the privacy statement when it appeared$/, async (t: TestCont
const elem = Selector(privacyModale, t);
const acceptButton = Selector('#L2AGLb > div', t);
await t.expect(elem.exists).ok('The privacy statement should be displayed', { timeout: 5000 }).click(acceptButton);

// forcefully remove as it seems google knows when an automated session is running
// await removeElement(t, privacyModale);
});

// reusing logic example
const searchOnGoogle = async (t: TestController, search?: string) => {
const input = Selector('[name="q"]', t);
const searchKeyword = search || t.ctx.hookValue;

await t
.expect(searchKeyword)
.ok()
.typeText(input, search || t.ctx.hookValue);
};
/* ACTIONS */

When(/^I type my search request "(.+)" on Google$/, async (t: TestController, [searchRequest]: string[]) => {
await searchOnGoogle(t, searchRequest);
Expand All @@ -49,12 +41,7 @@ When(/^I press the "(.+)" key$/, async (t: TestController, [key]: string[]) => {
await t.pressKey(key);
});

const expectGoogleResult = async (t: TestController, result?: string) => {
const firstLink = Selector('#rso', t).find('a');
const searchResult = result || t.ctx.hookValue;

await t.expect(searchResult).ok().expect(firstLink.innerText).contains(searchResult);
};
/* ASSERTIONS */

Then(
/^I should see that the first Google's result is "(.+)"$/,
Expand All @@ -66,3 +53,23 @@ Then(
Then(/^I should see that the first Google's result is as expected$/, async (t: TestController) => {
await expectGoogleResult(t);
});

/* HELPERS */

// reusing logic example
const searchOnGoogle = async (t: TestController, search?: string) => {
const input = Selector('[name="q"]', t);
const searchKeyword = search || t.ctx.hookValue;

await t
.expect(searchKeyword)
.ok()
.typeText(input, search || t.ctx.hookValue);
};

const expectGoogleResult = async (t: TestController, result?: string) => {
const firstLink = Selector('#rso', t).find('a');
const searchResult = result || t.ctx.hookValue;

await t.expect(searchResult).ok().expect(firstLink.innerText).contains(searchResult);
};
7 changes: 7 additions & 0 deletions examples/hooks-const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const BEFORE_PREFIX = 'before';
export const AFTER_PREFIX = 'after';
export const BEFORE_AND_AFTER_PREFIX = 'beforeafter';

export const beforeTags = ['tag1', 'tag2'].map((tagSuffix) => `${BEFORE_PREFIX}${tagSuffix}`);
export const afterTags = ['tag'].map((tagSuffix) => `${AFTER_PREFIX}${tagSuffix}`);
export const bothTags = ['tag'].map((tagSuffix) => `${BEFORE_AND_AFTER_PREFIX}${tagSuffix}`);
20 changes: 20 additions & 0 deletions examples/hooks-global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AfterAll, BeforeAll } from '@cucumber/cucumber';

let finishedFeaturesCount = 0;

BeforeAll(async (ctx, meta) => {
console.log('Preparing feature:', meta.name);
// set the fixtureCtx for all the scenarios included in the feature
ctx.featureName = meta.name;
ctx.afterHooksCounter = 0;
ctx.finishedFeaturesCount = finishedFeaturesCount;
});

AfterAll(async (ctx, meta) => {
const expectedCount = 3;
if (meta.name === 'Hooks feature 2' && ctx.afterHooksCounter !== expectedCount) {
throw new Error(`The After hooks ran ${ctx.afterHooksCounter} times instead of ${expectedCount}`);
}
finishedFeaturesCount += 1;
console.log('Cleaning after feature:', meta.name);
});
147 changes: 147 additions & 0 deletions examples/hooks-steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Given, Then } from '@cucumber/cucumber';

import { AFTER_PREFIX, BEFORE_PREFIX, beforeTags, afterTags, bothTags } from './hooks-const';

/* PREREQUISITES */

Given(/a (BeforeAll|AfterAll) hook is defined/, async (t: TestController) => {
// NOTE: nothing to do here as there is no way to check for global hooks being implemented
});

Given(/the scenario has no tags/, async (t: TestController) => {
const testTags: string[] = getTagsFromMeta(t);
// fails if any tag is added to the scenario
await t.expect(testTags.length).eql(0, `At least one tag (${testTags}) is attached to the scenario`);
});

Given(/the scenario has a tag linked to a Before hook/, async (t: TestController) => {
// fails in any case except a single tag that contains "before"
const testTags: string[] = getTagsFromMeta(t);
await t
.expect(testTags.length)
.eql(1, `Tag count is ${testTags.length}`)
.expect(beforeTags)
.contains(testTags[0], `${testTags[0]} doesn't exist in the hooks-tag file`);
});

Given(/the scenario has one tag linked to a Before hook/, async (t: TestController) => {
// fails in any case except a single tag that contains "before"
const testTags: string[] = getTagsFromMeta(t).filter((tag) => tag.includes(BEFORE_PREFIX));
await t
.expect(testTags.length)
.eql(1, `Tag count is ${testTags.length}`)
.expect(beforeTags)
.contains(testTags[0], `${testTags[0]} doesn't exist in the hooks-tag file`);
});

Given(/the scenario has one tag linked to an After hook/, async (t: TestController) => {
// fails in any case except a single tag that contains "before"
const testTags: string[] = getTagsFromMeta(t).filter((tag) => tag.includes(AFTER_PREFIX));
await t
.expect(testTags.length)
.eql(1, `Tag count is ${testTags.length}`)
.expect(afterTags)
.contains(testTags[0], `${testTags[0]} doesn't exist in the hooks-tag file`);
});

Given(/the scenario has a tag linked to an After hook/, async (t: TestController) => {
// fails in any case except a single tag that contains "after"
const testTags: string[] = getTagsFromMeta(t);
await t
.expect(testTags.length)
.eql(1, `Tag count is ${testTags.length}`)
.expect(afterTags)
.contains(testTags[0], `${testTags[0]} doesn't exist in the hooks-tag file`);
});

Given(/the scenario has several tags linked to Before hooks/, async (t: TestController) => {
// Cause the test to fail if the number of tags is less than 2
const testTags: string[] = getTagsFromMeta(t);
await t.expect(testTags.length).gte(2, `Tag count is ${testTags.length}`);

await Promise.all(
testTags.map(async (tag) => t.expect(beforeTags).contains(tag, `${tag} doesn't exist in the hooks-tag file`))
);
});

Given(/the scenario has a tag linked to a Before and an After hook/, async (t: TestController) => {
// fails in any case except a single tag that contains "both"
const testTags: string[] = getTagsFromMeta(t);
await t
.expect(testTags.length)
.eql(1, `Tag count is ${testTags.length}`)
.expect(bothTags)
.contains(testTags[0], `${testTags[0]} doesn't exist in the hooks-tag file`);
});

/* ASSERTIONS */

Then(/the BeforeAll hook should have run before any scenario/, async (t: TestController) => {
const fixtureName = t.testRun.test.fixture.name;
// fixtureCtx.featureName is set by the BeforeAll hook (see hooks-global.ts)
// Will cause the test to fail if the BeforeAll hook hasn't run
await t
.expect(fixtureName)
.ok("fixtureName doesn't exist in the test object")
.expect(fixtureName)
.contains(t.fixtureCtx.featureName, `featureName is not properly set to the current feature name`);
});

Then(/the AfterAll hook should have run after the previous feature/, async (t: TestController) => {
// cause the test to fail if the featureCounter hasn't been initialized yet
const { finishedFeaturesCount } = t.testRun.fixtureCtx;
await t
.expect(typeof finishedFeaturesCount)
.eql('number', `The finishedFeatureCount is of type ${typeof finishedFeaturesCount} instead of number`)
.expect(finishedFeaturesCount)
.gt(0, "No feature was finished or AfterAll didn't run");
});

Then(/no tagged hook should run/, async (t: TestController) => {
const contextKeys = Object.keys(t.ctx);
const beforeHookTrackers = contextKeys.filter((contextKey) => contextKey.includes(BEFORE_PREFIX));
const afterHookTrackers = contextKeys.filter((contextKey) => contextKey.includes(AFTER_PREFIX));
await t.expect(beforeHookTrackers.length).eql(0, `${beforeHookTrackers} hooks have run`);
await t.expect(afterHookTrackers.length).eql(0, `${afterHookTrackers} hooks have run`);
});

Then(/the linked Before hook should have run/, async (t: TestController) => {
// Cause the test to fail if no before hook ran or if the wrong one did
const selectedHook = Object.keys(t.ctx).filter((contextKey) => contextKey.includes(BEFORE_PREFIX));
const testTags = getTagsFromMeta(t);
await t
.expect(selectedHook.length)
.eql(
1,
`${selectedHook.length || 'No'} "${BEFORE_PREFIX}" key ${
selectedHook.length > 1 ? 'were' : 'was'
} found in the context`
)
.expect(testTags)
.contains(selectedHook[0], `${selectedHook[0]} is not associated to the current scenario`);
});

Then(/the linked After hook should run/, async (t: TestController) => {
// NOTE: cannot be tested in the current test because the hooks runs after it
// Instead the AfterAll hook checks if the After hook ran the correct amount of times
});

Then(/the linked Before hooks should have run/, async (t: TestController) => {
// Cause the test to fail if no before hook ran
const selectedHooks = Object.keys(t.ctx).filter((contextKey) => contextKey.includes(BEFORE_PREFIX));
await t.expect(selectedHooks.length).gt(0, `No key containing ${BEFORE_PREFIX} was found in the context`);

const testTags: string[] = getTagsFromMeta(t);

await Promise.all(
testTags.map(async (tag) => t.expect(selectedHooks).contains(tag, `The hook associated to ${tag} hasn't run`))
);
});

/* HELPERS */
const getTagsFromMeta = (t: TestController): string[] => {
return t.testRun.test.meta.tags
.split(',')
.filter((tag) => tag)
.map((tag) => tag.replace('@', ''));
};
17 changes: 17 additions & 0 deletions examples/hooks-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { After, Before } from '@cucumber/cucumber';
import { afterTags, beforeTags, bothTags } from './hooks-const';

const allBeforeTags = [...beforeTags, ...bothTags];
const allAfterTags = [...afterTags, ...bothTags];

allBeforeTags.forEach((fullTag) =>
Before(`@${fullTag}`, async (t: TestController) => {
t.ctx[fullTag] = true;
})
);

allAfterTags.forEach((fulltag) => {
After(`@${fulltag}`, async (t: TestController) => {
t.testRun.fixtureCtx.afterHooksCounter += 1;
});
});
20 changes: 20 additions & 0 deletions examples/hooks1.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: Hooks feature 1
I want to be able to use hooks to link specific behaviors to my scenarios

Scenario: Global hooks run before or after each feature
Given a BeforeAll hook is defined
Then the BeforeAll hook should have run before any scenario

Scenario: No tag
Given the scenario has no tags
Then no tagged hook should run

@beforetag1
Scenario: One tag - before
Given the scenario has a tag linked to a Before hook
Then the linked Before hook should have run

@beforetag1 @beforetag2
Scenario: Several tags
Given the scenario has several tags linked to Before hooks
Then the linked Before hooks should have run
24 changes: 24 additions & 0 deletions examples/hooks2.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Feature: Hooks feature 2
I want to be able to use hooks to link specific behaviors to my scenarios

Scenario: Global hooks run before or after each feature
Given a AfterAll hook is defined
Then the AfterAll hook should have run after the previous feature

@aftertag
Scenario: One tag - after
Given the scenario has a tag linked to an After hook
Then the linked After hook should run

@beforeaftertag
Scenario: One tag - before and after
Given the scenario has a tag linked to a Before and an After hook
Then the linked Before hook should have run
And the linked After hook should run

@beforetag1 @aftertag
Scenario: Several tags - before and after
Given the scenario has one tag linked to a Before hook
And the scenario has one tag linked to an After hook
Then the linked Before hook should have run
And the linked After hook should run
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ declare module 'gherkin-testcafe' {
developmentMode?: boolean
): Promise<GherkinTestCafe>;
}

interface TestController {
testRun: {
test: {
name: string;
meta: Record<string, any>;
fixture: {
name: string;
};
};
/**
* Shared between all the tests of a given feature - use carefully as sharing data between
* tests goes against the isolation principle
*/
fixtureCtx: Record<string, any>;
};
}
}

const createTestCafe: GherkinTestCafeFactory;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"custom-param-type-example": "node main.js chrome examples/google.ts ./examples/custom-param-type.* --param-type-registry-file ./examples/custom-param-type-registry.js",
"http-auth-example": "node main.js chrome ./examples/http-authentication-example.*",
"error-reporting-example": "node main.js chrome ./examples/error-reporting* ./examples/google.ts",
"hooks-example": "node main.js chrome ./examples/hooks*",
"tags-1-example": "node main.js chrome ./examples/tags.* --tags @scenarioTag1",
"tags-not1-example": "node main.js chrome ./examples/tags.* --tags ~@scenarioTag1",
"tags-1or2-example": "node main.js chrome ./examples/tags.* --tags @scenarioTag1,@scenarioTag2",
Expand Down

0 comments on commit 293dd77

Please sign in to comment.