Framework Structure & Configuration

Introduction

In this section, we will begin to add our folders and files that our framework will need. We will also add a .gitignore file so that if you decide to put your framework in a Git repository, you are able to without unnecessary files being included in the repository.

Adding a .gitignore file

  1. Open up a terminal (or command prompt) and change directory (cd) in to your project root…
    cd your/project/root

    (change ‘your/project/root’ to the path of your local project’s root)

  2. First, let’s initialise an empty git repository…
    git init
  3. Next, create (touch) a .gitignore file…
    touch .gitignore
  4. Next, go into the nano editor for the .gitignore file we just created…
    nano .gitignore
  5. Add the following lines in your .gitignore file to ignore the packages in our ‘/node_modules’ folder as well as any folders specific to our code editor (e.g. ‘.vscode/’)…
    pending-steps.txt
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    lerna-debug.log*
    
    # Diagnostic reports (https://nodejs.org/api/report.html)
    report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
    
    # Runtime data
    pids
    *.pid
    *.seed
    *.pid.lock
    
    # Directory for instrumented libs generated by jscoverage/JSCover
    lib-cov
    
    # Coverage directory used by tools like istanbul
    coverage
    *.lcov
    
    # nyc test coverage
    .nyc_output
    
    # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
    .grunt
    
    # Bower dependency directory (https://bower.io/)
    bower_components
    
    # node-waf configuration
    .lock-wscript
    
    # Compiled binary addons (https://nodejs.org/api/addons.html)
    build/Release
    
    # Dependency directories
    node_modules/
    jspm_packages/
    
    # TypeScript v1 declaration files
    typings/
    
    # TypeScript cache
    *.tsbuildinfo
    
    # Optional npm cache directory
    .npm
    
    # Optional eslint cache
    .eslintcache
    
    # Microbundle cache
    .rpt2_cache/
    .rts2_cache_cjs/
    .rts2_cache_es/
    .rts2_cache_umd/
    
    # Optional REPL history
    .node_repl_history
    
    # Output of 'npm pack'
    *.tgz
    
    # Yarn Integrity file
    .yarn-integrity
    
    # dotenv environment variables file
    .env
    .env.test
    
    # parcel-bundler cache (https://parceljs.org/)
    .cache
    
    # Next.js build output
    .next
    
    # Nuxt.js build / generate output
    .nuxt
    dist
    
    # Gatsby files
    .cache/
    # Comment in the public line in if your project uses Gatsby and not Next.js
    # https://nextjs.org/blog/next-9-1#public-directory-support
    # public
    
    # vuepress build output
    .vuepress/dist
    
    # Serverless directories
    .serverless/
    
    # FuseBox cache
    .fusebox/
    
    # DynamoDB Local files
    .dynamodb/
    
    # TernJS port file
    .tern-port
    
    # Stores VSCode versions used for testing VSCode extensions
    .vscode-test

    You’re now ready to setup your own GitHub/GitLab repository, if you desire to do so (I won’t be covering that in this blog series).

Directory and File Structure

  1. Open up a terminal (or command prompt) and change directory (cd) in to your project root…
    cd your/project/root

    (change ‘your/project/root’ to the path of your local project’s root)

  2. First, let’s open our project root in VS Code…
    code .

Features

  1. In the Project Explorer in Visual Studio Code, add a ‘features’ folder to the project root by hovering the mouse over the project folder’s name and clicking the ‘New Folder’ icon
  2. Next, right-click the ‘features’ folder and add a new file called ‘find_a_restaurant.feature’ (don’t worry about the name, it will make sense later when we start to add our tests)

Steps

  1. Right-click the ‘features’ folder and add another sub-folder called ‘steps’, which is where all of our step definition files will go
  2. Next, right-click the ‘steps’ folder and add a new file called ‘foodie.steps.ts’ (again, the naming of this file will make more sense when we start writing our tests)

Support

  1. Right-click the ‘features’ folder and add a new sub-folder called ‘support’

Spec

  1. In the Project Explorer in Visual Studio Code, add a ‘spec’ folder to the project root by hovering the mouse over the project folder’s name and clicking the ‘New Folder’ icon

Screenplay

  1. Right-click the ‘spec’ folder and add another sub-folder called ‘screenplay’, which is where we will keep our screenplay related classes later on in this series

We will add the rest of the files and folders in later sections when we start to properly dig in to the Screenplay Pattern and its principles 🙂

Configuration

We will now add necessary configuration files for Protractor and TypeScript, as well as tweak the package.json file 🙂

protractor.conf.js

We will now create a ‘protractor.conf.js‘ file that will hold all of our Protractor configuration settings

  1. With Visual Studio Code open, add a new file to the project root called ‘protractor.conf.js
  2. Open up the ‘protractor.conf.js‘ file and add the following configuration code (I have added comments to try and explain what is going on)…
    exports.config = { // export the config so it is accessible by other files
        baseUrl: 'https://www.zagat.com', // set the base url of our system under test
        directConnect: true, // your test script communicates directly Chrome Driver or Firefox Driver, bypassing any Selenium Server.
        allScriptsTimeout: 120000, // set global protractor timeout to 120 seconds (2 minutes)
        capabilities: {
            browserName: 'chrome' // set test browser to 'chrome', can also be set to 'firefox' as well as some others
        },
        specs: ['./features/*.feature'], // set the location of our tests to any and all feature files in the 'features' folder
        framework: 'custom',
        frameworkPath: require.resolve('@serenity-js/protractor/adapter'), // this serenity-js module needs to be called as a custom framework
        onPrepare: () => {
            require('ts-node').register({ //register 'ts-node' to resolve all our package imports
                project: './tsconfig.json' // and handle TypeScript execution smoothly
            });
        },
        cucumberOpts: {
            require: ['./features/steps/*.steps.ts'], // require our step definition files
            format: ['snippets:pending-steps.txt'], // workaround to get pending step definitions
            compiler: 'ts:ts-node/register' // interpret step definitions as TypeScript
        }
    };

tsconfig.json

We will now create a ‘tsconfig.json’ file that allows you to specify the root level files and the compiler options that are required to compile a TypeScript project. The presence of this file in a directory specifies that the said directory is the TypeScript project root.

  1. With Visual Studio Code open, add a new file to the project root called ‘tsconfig.json’
  2. Open up the file and paste in the following, the uncommented lines are the only ones we need for now, however I have included all the other options commented out so you can see what they are…
    {
        "compilerOptions": {
          /* Basic Options */
          // "incremental": true,                   /* Enable incremental compilation */
          "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
          "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
          // "lib": [],                             /* Specify library files to be included in the compilation. */
          // "allowJs": true,                       /* Allow javascript files to be compiled. */
          // "checkJs": true,                       /* Report errors in .js files. */
          // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
          "declaration": false,                   /* Generates corresponding '.d.ts' file. */
          // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
          "sourceMap": true,                     /* Generates corresponding '.map' file. */
          // "outFile": "./",                       /* Concatenate and emit output to single file. */
          "outDir": "./staging/transpiled",        /* Redirect output structure to the directory. */
          "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
          // "composite": true,                     /* Enable project compilation */
          // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
          "removeComments": false,                /* Do not emit comments to output. */
          // "noEmit": true,                        /* Do not emit outputs. */
          // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
          // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
          // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
      
          /* Strict Type-Checking Options */                         /* Enable all strict type-checking options. */
          "noImplicitAny": false,                 /* Raise error on expressions and declarations with an implied 'any' type. */
          // "strictNullChecks": true,              /* Enable strict null checks. */
          // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
          // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
          // "strictPropertyInitialization": true,  /* Enable strict checking of property initialisation in classes. */
          // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
          // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
      
          /* Additional Checks */
          // "noUnusedLocals": true,                /* Report errors on unused locals. */
          // "noUnusedParameters": true,            /* Report errors on unused parameters. */
          // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
          // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
      
          /* Module Resolution Options */
          "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
          // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
          // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
          // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
          // "typeRoots": [],                       /* List of folders to include type definitions from. */
          // "types": [],                           /* Type declaration files to be included in compilation. */
          // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
          "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
          // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
          // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
      
          /* Source Map Options */
          // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
          // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
          // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
          // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
      
          /* Experimental Options */
          "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
          "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
      
          /* Advanced Options */
          "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
        },
        "include": [
            "features/**/*.ts", // include TypeScript files in the features directory and subdirectories
            "src/**/*.ts" // include TypeScript files in our 'src' directory and sub directories
        ],
        "exclude": [ 
            "node_modules" // exclude any TypeScript files in 'node_modules' directory
        ]
      }

package.json

Finally, we will tweak our ‘package.json’ file to add some scripts that will run our tests when we input ‘npm test’ in to the command line as well as run any ‘pretest’ scripts we may need (e.g. around ‘webdriver-manager update’) 🙂

  1. Open up the package.json file and edit the scripts block so it looks like below…
    "scripts": {
        "test": "protractor protractor.conf.js",
        "pretest": "npm run webdriver-update",
        "protractor": "./node_modules/.bin/protractor",
        "webdriver-update": "./node_modules/.bin/webdriver-manager update"
      },

    The whole thing should look similar to below…

    {
      "name": "serenity-js-framework",
      "version": "1.0.0",
      "description": "",
      "main": "protractor.conf.ts",
      "scripts": {
        "test": "protractor protractor.conf.js",
        "pretest": "npm run webdriver-update",
        "protractor": "./node_modules/.bin/protractor",
        "webdriver-update": "./node_modules/.bin/webdriver-manager update"
      },
      "repository": {
        "type": "git",
        "url": "git+https://gitlab.com/testifyqa/javascript-web-serenityjs.git"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "bugs": {
        "url": "https://gitlab.com/testifyqa/javascript-web-serenityjs/issues"
      },
      "homepage": "https://gitlab.com/testifyqa/javascript-web-serenityjs#readme",
      "devDependencies": {
        "eslint": "^6.8.0"
      },
      "dependencies": {
        "@serenity-js/assertions": "^2.0.1-alpha.109",
        "@serenity-js/console-reporter": "^2.0.1-alpha.109",
        "@serenity-js/core": "^2.0.1-alpha.109",
        "@serenity-js/cucumber": "^2.0.1-alpha.109",
        "@serenity-js/protractor": "^2.0.1-alpha.109",
        "@serenity-js/serenity-bdd": "^2.0.1-alpha.109",
        "@types/cucumber": "^6.0.0",
        "@types/node": "^13.5.0",
        "@typescript-eslint/eslint-plugin": "^2.17.0",
        "@typescript-eslint/parser": "^2.17.0",
        "cucumber": "^6.0.5",
        "glob": "^7.1.6",
        "protractor": "^5.4.2",
        "ts-node": "^8.6.2",
        "typescript": "^3.6.4"
      }
    }

If we now run npm test, it will first run the “pretest” script we set up that runs ‘webdriver-manager update’ and then run the test script via protractor (even though we currently only have a blank feature file). We should see the Chrome browser launch and then close 🙂

Serenity/JS configuration

We will now start to setup the configuration for Screenplay Pattern, by adding a ‘serenity{}’ block in our ‘protractor.conf.ts’ file

Choosing our runner

Almost like a runner in film post-production, let’s configure our runner to use Cucumber 🙂

  1. Open up the ‘protractor.conf.js‘ file and add the following serenity configuration block to set the runner…
    serenity: {
        runner: 'cucumber' // tell serenity-js that we want to use Cucumber runner
    },

    The whole thing should look like below…

    const
        { Photographer, TakePhotosOfFailures } = require('@serenity-js/protractor'),
        { SerenityBDDReporter } = require('@serenity-js/serenity-bdd');
    
    export config: Config = { //create an object of Config called 'config' and export it so it is accessible by other files
        baseUrl: 'https://www.zagat.com', // set the base url of our system under test
        directConnect: true, //Your test script communicates directly Chrome Driver or Firefox Driver, bypassing any Selenium Server.
        allScriptsTimeout: 120000, // set global protractor timeout to 120 seconds (2 minutes)
        capabilities: {
            browserName: 'chrome' //set test browser to 'chrome', can also be set to 'firefox' as well as some others
        },
        specs: ['./features/*.feature'], // set the location of our tests to any and all feature files in the 'features' folder
        framework: 'custom',
        frameworkPath: require.resolve('@serenity-js/protractor/adapter'), //this serenity-js module needs to be called as a custom framework
        onPrepare: () => {
            require('ts-node').register({ // register 'ts-node' to resolve all our package imports
                project: './tsconfig.json' // and handle TypeScript execution smoothly
            });
        }
        serenity: {
            runner: 'cucumber'
        },
        cucumberOpts: {
            require: ['./features/steps/*.steps.ts', // require our step definition files
                      './support/**/*.ts'], // also require any necessary files in support folders and sub-folders (e.g. setup.ts)
            format: ['snippets:pending-steps.txt'], // workaround to get pending step definitions
            compiler: 'ts:ts-node/register' //interpret step definitions as TypeScript
        }
    };

Choosing our Stage Crew

The Stage Crew Members listen on and act upon the domain events emitted during the execution of a test scenario. By default, our stage crew normally consists of a photographer who will take photos of interactions (e.g. take screenshots during our tests) and a Serenity BDD reporter, who produces Serenity BDD-compatible JSON reports.

Let’s add these default workers to our stage crew now 🙂

We will also add a console reporter so we can see the execution summary in the console when we run tests. I use a dark terminal, but you can change ‘Dark’ for ‘Light’ or leave it as the default like I am using below.

Lastly, we will ad an artifact archiver to store our reports at the default location of ‘./target/site/serenity’

  1. Open up the ‘protractor.conf.ts’ file and add the stage crew members to the serenity block in a ‘crew’ array, as well as adding the appropriate imports at the top of the file, with the whole thing looking similar to below…
    const
        { Photographer, TakePhotosOfFailures } = require('@serenity-js/protractor'),
        { SerenityBDDReporter } = require('@serenity-js/serenity-bdd'),
        { ConsoleReporter } = require('@serenity-js/console-reporter'),
        { ArtifactArchiver } = require('@serenity-js/core');
    
    exports.config = { // export the config so it is accessible by other files
        baseUrl: 'https://www.zagat.com',
        directConnect: true, // your test script communicates directly Chrome Driver or Firefox Driver, bypassing any Selenium Server.
        allScriptsTimeout: 120000, // set global protractor timeout to 120 seconds (2 minutes)
        capabilities: {
            browserName: 'chrome' // set test browser to 'chrome', can also be set to 'firefox' as well as some others
        },
        specs: ['./features/*.feature'], // set the location of our tests to any and all feature files in the 'features' folder
        framework: 'custom',
        frameworkPath: require.resolve('@serenity-js/protractor/adapter'), // this serenity-js module needs to be called as a custom framework
        onPrepare: () => {
            require('ts-node').register({ // register 'ts-node' to resolve all our package imports
                project: './tsconfig.json' // and handle TypeScript execution smoothly
            });
        },
        serenity: {
            runner: 'cucumber', // tell serenity-js that we want to use the Cucumber runner
            crew: [
                Photographer.whoWill(TakePhotosOfFailures), // or Photographer.whoWill(TakePhotosOfInteractions),
                new SerenityBDDReporter(),
                ConsoleReporter.forDarkTerminals(), // display execution summary in console
                ArtifactsArchiver.storingArtifactsAt('./target/site/serenity') // store serenity reports at given location
            ]
        },
        cucumberOpts: {
            require: ['./features/**/*.ts', //require our step definition files
                      './support/**/*.ts'], // also require any necessary files in support folders and sub-folders (e.g. setup.ts)        format: ['snippets:pending-steps.txt'], // workaround to get pending step definitions
            compiler: 'ts:ts-node/register' //interpret step definitions as TypeScript
        },
    };

Setting up Debug capabilities in Visual Studio Code

We will now set up our Visual Studio Code so that we can debug our tests with breakpoints by simply pressing F5 🙂

  1. With the project open in Visual Studio Code, click the Debug icon in the left menu bar (the icon looks like a bug/insect)
  2. Next, click the ‘create a launch.json file’ link
  3. Edit the ‘launch.json’ file to look like below…
    {
        // Use IntelliSense to learn about possible attributes.
        // Hover to view descriptions of existing attributes.
        // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
        "version": "0.2.0",
        "configurations": [
            {
                "type": "node",
                "request": "launch",
                "name": "Launch Program",
                "program": "${workspaceFolder}/node_modules/protractor/bin/protractor",
                "args": ["${workspaceFolder}/protractor.conf.js"],
                "outFiles": [
                    "${workspaceFolder}/**/*.js"
                ]
            }
        ]
    }

We should now be setup with everything configured correctly. In the next section, we will start really dig in to the Screenplay Pattern and how it works 😀

Liked it? Take a second to support Thomas on Patreon!

Previous Article

Next Article

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.