diff --git a/examples/google.ts b/examples/google.ts index 36062e0..b0ee2c5 100644 --- a/examples/google.ts +++ b/examples/google.ts @@ -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 }); @@ -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'); }); @@ -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); @@ -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 "(.+)"$/, @@ -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); +}; diff --git a/examples/hooks-const.ts b/examples/hooks-const.ts new file mode 100644 index 0000000..0575195 --- /dev/null +++ b/examples/hooks-const.ts @@ -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}`); diff --git a/examples/hooks-global.ts b/examples/hooks-global.ts new file mode 100644 index 0000000..3eab242 --- /dev/null +++ b/examples/hooks-global.ts @@ -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); +}); diff --git a/examples/hooks-steps.ts b/examples/hooks-steps.ts new file mode 100644 index 0000000..79da23c --- /dev/null +++ b/examples/hooks-steps.ts @@ -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('@', '')); +}; diff --git a/examples/hooks-tag.ts b/examples/hooks-tag.ts new file mode 100644 index 0000000..e253b55 --- /dev/null +++ b/examples/hooks-tag.ts @@ -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; + }); +}); diff --git a/examples/hooks1.feature b/examples/hooks1.feature new file mode 100644 index 0000000..74e9c58 --- /dev/null +++ b/examples/hooks1.feature @@ -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 diff --git a/examples/hooks2.feature b/examples/hooks2.feature new file mode 100644 index 0000000..290648e --- /dev/null +++ b/examples/hooks2.feature @@ -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 diff --git a/index.d.ts b/index.d.ts index e490632..1a0ea12 100644 --- a/index.d.ts +++ b/index.d.ts @@ -32,6 +32,23 @@ declare module 'gherkin-testcafe' { developmentMode?: boolean ): Promise; } + + interface TestController { + testRun: { + test: { + name: string; + meta: Record; + 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; + }; + } } const createTestCafe: GherkinTestCafeFactory; diff --git a/package.json b/package.json index c5795fe..18dbc58 100644 --- a/package.json +++ b/package.json @@ -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",