├── .npmrc ├── step-filters ├── index.ts ├── README.md └── env.ts ├── models ├── index.ts └── sample-page-model.ts ├── selectors ├── index.ts └── testcafe-sample-page-selectors.ts ├── tools ├── string │ ├── symbols.ts │ ├── upper-case-first-letter.ts │ └── surround-with-quote.ts ├── fs │ ├── ignore-node-modules.ts │ ├── is-file.ts │ ├── slash.ts │ ├── is-directory.ts │ ├── get-filename.ts │ ├── read-all-lines-in-file.ts │ ├── ignore-dot-dir.ts │ ├── is-step-file.ts │ ├── ignore-dir-that-is-in.ts │ ├── file-exists.ts │ ├── get-files-in-directory.ts │ ├── get-filepath-without-extension.ts │ ├── ensure-directory-structure-exists.ts │ ├── get-directories-recursively-in.ts │ ├── get-func-name-from-file-name.ts │ ├── get-directories-in.ts │ ├── get-exported-functions-in.ts │ ├── get-relative-path-from.ts │ └── get-jsdoc-of-function.ts └── regex-match.ts ├── .media ├── demo1.gif ├── demo2.gif ├── demo3.gif ├── report01.png ├── screenshot01.png ├── screenshot02.png ├── screenshot03.png ├── screenshot04.png ├── screenshot05.png ├── screenshot06.png ├── screenshot07.png ├── screenshot08.png └── screenshot09.png ├── .prettierignore ├── .eslintignore ├── .prettierrc ├── step-templates ├── not-implemented-step.ts └── basic-template-step.ts ├── steps ├── no-name-should-be-populated.ts ├── i-can-submit-my-feedback-on-testcafe.ts ├── i-cannot-submit-my-feedback-on-testcafe.ts ├── i-send-my-feedback-on-testcafe.ts ├── i-navigate-to-the-testcafe-sample-page.ts ├── i-enter-my-name.ts ├── a-xxx-message-should-appear-with-my-name.ts ├── i-do-something-specific.ts └── README.md ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── testcafe-reporter-cucumber-json.json ├── config ├── default-config.ts ├── config.interface.ts ├── parsed-config.ts ├── personas.ts ├── environments.ts ├── README.md └── testcafe-config.ts ├── appveyor.yml ├── step-mappings ├── README.md ├── steps.ts ├── config.ts ├── index.ts └── generator │ ├── index.ts │ ├── create-steps-barrel.ts │ └── create-steps-mappings.ts ├── report-generator.ts ├── LICENSE ├── .travis.yml ├── .gitignore ├── features ├── testcafe-sample-page.spec.ts └── README.md ├── .eslintrc.json ├── CHANGELOG.md ├── package.json ├── step-runner.ts ├── tsconfig.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true -------------------------------------------------------------------------------- /step-filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env'; 2 | -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sample-page-model'; 2 | -------------------------------------------------------------------------------- /selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './testcafe-sample-page-selectors'; 2 | -------------------------------------------------------------------------------- /tools/string/symbols.ts: -------------------------------------------------------------------------------- 1 | export const symbols = { 2 | err: '✖', 3 | ok: '✓', 4 | }; 5 | -------------------------------------------------------------------------------- /.media/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/demo1.gif -------------------------------------------------------------------------------- /.media/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/demo2.gif -------------------------------------------------------------------------------- /.media/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/demo3.gif -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | package 3 | reports 4 | step-mappings/index.ts 5 | step-mappings/steps.ts -------------------------------------------------------------------------------- /.media/report01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/report01.png -------------------------------------------------------------------------------- /.media/screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot01.png -------------------------------------------------------------------------------- /.media/screenshot02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot02.png -------------------------------------------------------------------------------- /.media/screenshot03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot03.png -------------------------------------------------------------------------------- /.media/screenshot04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot04.png -------------------------------------------------------------------------------- /.media/screenshot05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot05.png -------------------------------------------------------------------------------- /.media/screenshot06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot06.png -------------------------------------------------------------------------------- /.media/screenshot07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot07.png -------------------------------------------------------------------------------- /.media/screenshot08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot08.png -------------------------------------------------------------------------------- /.media/screenshot09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdorgeval/testcafe-starter/HEAD/.media/screenshot09.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | **/vendor/*.js 3 | build 4 | coverage 5 | node_modules 6 | reports 7 | cucumber-json-reports -------------------------------------------------------------------------------- /tools/fs/ignore-node-modules.ts: -------------------------------------------------------------------------------- 1 | export const ignoreNodeModules = (path: string): boolean => path.indexOf('node_modules') < 0; 2 | -------------------------------------------------------------------------------- /tools/fs/is-file.ts: -------------------------------------------------------------------------------- 1 | import { PathLike, statSync } from 'fs'; 2 | 3 | export const isFile = (path: PathLike): boolean => statSync(path).isFile(); 4 | -------------------------------------------------------------------------------- /tools/fs/slash.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | export const slash: (path: string) => string = require('slash'); 3 | -------------------------------------------------------------------------------- /tools/fs/is-directory.ts: -------------------------------------------------------------------------------- 1 | import { PathLike, statSync } from 'fs'; 2 | 3 | export const isDirectory = (path: PathLike): boolean => statSync(path).isDirectory(); 4 | -------------------------------------------------------------------------------- /models/sample-page-model.ts: -------------------------------------------------------------------------------- 1 | export interface PageModel { 2 | name?: string; 3 | remoteTesting?: boolean; 4 | } 5 | 6 | export const pageModel: PageModel = { 7 | name: 'john doe', 8 | }; 9 | -------------------------------------------------------------------------------- /tools/fs/get-filename.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'path'; 2 | export const getFileName = (path: string): string | undefined => { 3 | /* prettier-ignore */ 4 | return path 5 | .split(sep) 6 | .pop(); 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "printWidth": 100, 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "es5", 9 | "useTabs": false 10 | } -------------------------------------------------------------------------------- /step-templates/not-implemented-step.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @step 3 | * @given("I do something") 4 | */ 5 | export default async (stepName: string): Promise => { 6 | throw new Error(`Step "${stepName}" is not yet implemented.`); 7 | }; 8 | -------------------------------------------------------------------------------- /tools/fs/read-all-lines-in-file.ts: -------------------------------------------------------------------------------- 1 | import { PathLike, readFileSync } from 'fs'; 2 | 3 | export const readAllLinesInFile = (filePath: PathLike): string[] => { 4 | const lines = readFileSync(filePath, 'utf8').split(/\n|\r/); 5 | return lines; 6 | }; 7 | -------------------------------------------------------------------------------- /tools/fs/ignore-dot-dir.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'path'; 2 | 3 | export const ignoreDotDir = (path: string): boolean => { 4 | return ( 5 | path.split(sep).filter((folderName: string): boolean => folderName.startsWith('.')).length === 0 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /steps/no-name-should-be-populated.ts: -------------------------------------------------------------------------------- 1 | import * as selector from '../selectors'; 2 | import { t } from 'testcafe'; 3 | 4 | /** 5 | * @step 6 | * @then("no name should be populated") 7 | */ 8 | export default async (): Promise => { 9 | await t.expect(selector.userNameInputBox.value).eql(''); 10 | }; 11 | -------------------------------------------------------------------------------- /tools/string/upper-case-first-letter.ts: -------------------------------------------------------------------------------- 1 | export const upperCaseFirstLetter = (input: string): string => { 2 | return [...input] 3 | .map((char: string, charIndex): string => { 4 | if (charIndex === 0) { 5 | return char.toUpperCase(); 6 | } 7 | return char; 8 | }) 9 | .join(''); 10 | }; 11 | -------------------------------------------------------------------------------- /selectors/testcafe-sample-page-selectors.ts: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | export const firstInputBox = Selector('input[type=text]').nth(0); 3 | export const secondInputBox = Selector('input[type=text]').nth(1); 4 | export const userNameInputBox = Selector('input#developer-name[type=text]'); 5 | export const submitButton = Selector('button[type=submit]'); 6 | export const resultContent = Selector('div.result-content'); 7 | -------------------------------------------------------------------------------- /tools/fs/is-step-file.ts: -------------------------------------------------------------------------------- 1 | import { readAllLinesInFile } from './read-all-lines-in-file'; 2 | import { PathLike } from 'fs'; 3 | 4 | export const isStepFile = (path: PathLike): boolean => { 5 | try { 6 | /* prettier-ignore */ 7 | return readAllLinesInFile(path) 8 | .filter((line: string): boolean => line.endsWith(' * @step')) 9 | .length > 0; 10 | } catch (error) { 11 | return false; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /tools/fs/ignore-dir-that-is-in.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'path'; 2 | 3 | export const ignoreDirThatIsIn = (folders: string[]): ((path: string) => boolean) => ( 4 | path: string 5 | ): boolean => { 6 | return ( 7 | path 8 | .split(sep) 9 | .filter( 10 | (folderName: string): boolean => 11 | folders.filter((folderToIgnore: string): boolean => folderToIgnore === folderName) 12 | .length > 0 13 | ).length === 0 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /tools/fs/file-exists.ts: -------------------------------------------------------------------------------- 1 | import { isDirectory } from './is-directory'; 2 | import { isFile } from './is-file'; 3 | import { existsSync } from 'fs'; 4 | 5 | export const fileExists = (filePath: string): boolean => { 6 | if (existsSync(filePath) && isFile(filePath)) { 7 | return true; 8 | } 9 | 10 | if (existsSync(filePath) && isDirectory(filePath)) { 11 | throw new Error(`File '${filePath}' is a directory but should be a file.`); 12 | } 13 | 14 | return false; 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "hdorgeval.testcafe-snippets", 7 | "romanresh.testcafe-test-runner", 8 | "esbenp.prettier-vscode", 9 | "dbaeumer.vscode-eslint", 10 | "oderwat.indent-rainbow", 11 | "coenraads.bracket-pair-colorizer-2", 12 | "wmaurer.change-case" 13 | ] 14 | } -------------------------------------------------------------------------------- /tools/fs/get-files-in-directory.ts: -------------------------------------------------------------------------------- 1 | import { isFile } from './is-file'; 2 | import { PathLike, readdirSync } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | const defaultFileFilter = (): boolean => true; 6 | 7 | export const getFilesInDirectory = ( 8 | path: PathLike, 9 | fileFilter?: (path: string) => boolean 10 | ): string[] => 11 | readdirSync(path) 12 | .map((name: string): string => join(path.toString(), name)) 13 | .filter(isFile) 14 | .filter(fileFilter || defaultFileFilter); 15 | -------------------------------------------------------------------------------- /tools/regex-match.ts: -------------------------------------------------------------------------------- 1 | export function firstMatch(regex: RegExp, value: string): string | null { 2 | const match = value.match(regex); 3 | if (match === null) { 4 | return null; 5 | } 6 | const result = match[0].replace(/'/gi, ''); 7 | return result; 8 | } 9 | export function secondMatch(regex: RegExp, value: string): string | null { 10 | const match = value.match(regex); 11 | if (match === null) { 12 | return null; 13 | } 14 | const result = match[1].replace(/'/gi, ''); 15 | return result; 16 | } 17 | -------------------------------------------------------------------------------- /testcafe-reporter-cucumber-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "noisyTags": [ 3 | "able", 4 | "and", 5 | "async", 6 | "but", 7 | "can", 8 | "cannot", 9 | "did", 10 | "feature", 11 | "fixture", 12 | "given", 13 | "not", 14 | "only", 15 | "scenario", 16 | "spec", 17 | "the", 18 | "then", 19 | "test", 20 | "with", 21 | "when", 22 | "(t)" 23 | ], 24 | "separators": [ 25 | ".", 26 | ":", 27 | "!", 28 | ",", 29 | ";" 30 | ], 31 | "verbose": false 32 | } -------------------------------------------------------------------------------- /config/default-config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './config.interface'; 2 | import { env } from './environments'; 3 | import { user } from './personas'; 4 | 5 | export const defaultConfig: Config = { 6 | env: env('local'), 7 | showConfig: true, 8 | testSpeed: 1.0, 9 | timeout: { 10 | longTimeout: 30000, 11 | shortTimeout: 5000, 12 | // insert your custom timeouts here 13 | ...{ 'on-waiting-remote-server-response': 180000 }, 14 | ...{ 'on-waiting-custom-event': 4000 }, 15 | }, 16 | user: user('user1@example.com'), 17 | }; 18 | -------------------------------------------------------------------------------- /tools/fs/get-filepath-without-extension.ts: -------------------------------------------------------------------------------- 1 | import { getFileName } from './get-filename'; 2 | import { parse, sep } from 'path'; 3 | 4 | export const getFilePathWithoutExtension = (path: string): string => { 5 | const fileName = getFileName(path); 6 | const filenameWithoutExtension = fileName ? parse(fileName).name : undefined; 7 | const foldersHierarchy = path.split(sep); 8 | foldersHierarchy.pop(); 9 | if (filenameWithoutExtension) { 10 | foldersHierarchy.push(filenameWithoutExtension); 11 | } 12 | return foldersHierarchy.join(sep); 13 | }; 14 | -------------------------------------------------------------------------------- /tools/string/surround-with-quote.ts: -------------------------------------------------------------------------------- 1 | export const surround = (input: string): { with: (quote: string) => string } => { 2 | return { 3 | with: (quote: string): string => { 4 | if (input.startsWith(quote) && input.endsWith(quote)) { 5 | return input; 6 | } 7 | if (input.includes(quote) && quote === "'") { 8 | return `"${input}"`; 9 | } 10 | 11 | if (input.includes(quote) && quote === '"') { 12 | return `'${input}'`; 13 | } 14 | 15 | return `${quote}${input}${quote}`; 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /steps/i-can-submit-my-feedback-on-testcafe.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentConfig } from '../config/testcafe-config'; 2 | import * as selector from '../selectors'; 3 | import { t } from 'testcafe'; 4 | 5 | /** 6 | * @step 7 | * @then("I can submit my feedback on testcafe") 8 | */ 9 | export default async (): Promise => { 10 | // get the config that was injected into the fixture/test context by the feature 11 | const config = getCurrentConfig(t); 12 | await t 13 | .expect(selector.submitButton.hasAttribute('disabled')) 14 | .notOk({ timeout: config.timeout.longTimeout }); 15 | }; 16 | -------------------------------------------------------------------------------- /config/config.interface.ts: -------------------------------------------------------------------------------- 1 | import { EnvInfo } from './environments'; 2 | import { UserInfo } from './personas'; 3 | 4 | export interface Config { 5 | env?: EnvInfo; 6 | user?: UserInfo; 7 | testSpeed: number; 8 | timeout: Timeout; 9 | showConfig: boolean; 10 | } 11 | 12 | export interface Timeout { 13 | [index: string]: number; 14 | longTimeout: number; 15 | shortTimeout: number; 16 | } 17 | 18 | export interface ParsedConfig { 19 | env?: EnvInfo; 20 | user?: UserInfo; 21 | 22 | testSpeed?: number; 23 | timeout?: Partial; 24 | showConfig?: boolean; 25 | } 26 | -------------------------------------------------------------------------------- /steps/i-cannot-submit-my-feedback-on-testcafe.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentConfig } from '../config/testcafe-config'; 2 | import * as selector from '../selectors'; 3 | import { t } from 'testcafe'; 4 | 5 | /** 6 | * @step 7 | * @then("I cannot submit my feedback on testcafe") 8 | */ 9 | export default async (): Promise => { 10 | // get the config that was injected into the fixture/test context by the feature 11 | const config = getCurrentConfig(t); 12 | await t 13 | .expect(selector.submitButton.hasAttribute('disabled')) 14 | .ok({ timeout: config.timeout.longTimeout }); 15 | }; 16 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '12' 4 | 5 | install: 6 | - ps: > 7 | if ($env:nodejs_version -eq "12") { 8 | Install-Product node $env:nodejs_version x64 9 | } else { 10 | Install-Product node $env:nodejs_version 11 | } 12 | - set PATH=%APPDATA%\npm;%PATH% 13 | - npm install 14 | 15 | matrix: 16 | fast_finish: false 17 | build: off 18 | shallow_clone: true 19 | test_script: 20 | - node --version 21 | - npm --version 22 | - npx --version 23 | - npm run build 24 | - npm test 25 | cache: 26 | - '%APPDATA%\npm-cache' 27 | -------------------------------------------------------------------------------- /tools/fs/ensure-directory-structure-exists.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import { sep } from 'path'; 3 | 4 | const ensureDirectoryExists = (directoryPath: string): void => { 5 | if (existsSync(directoryPath)) { 6 | return; 7 | } 8 | mkdirSync(directoryPath); 9 | }; 10 | export const ensureDirectoryStructureExists = (filePath: string): void => { 11 | const dirs = filePath.split(sep); 12 | dirs.pop(); 13 | 14 | let partialPath = '.'; 15 | dirs.forEach((dir: string): void => { 16 | partialPath = [partialPath, dir].join(sep); 17 | ensureDirectoryExists(partialPath); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /tools/fs/get-directories-recursively-in.ts: -------------------------------------------------------------------------------- 1 | import { FluentSyntaxForDirectoryFiltering, getDirectoriesIn } from './get-directories-in'; 2 | import { PathLike } from 'fs'; 3 | 4 | function getAllDirectoriesRecursivelyIn(path: PathLike): string[] { 5 | const subDirs = getDirectoriesIn(path).takeAll(); 6 | const result: string[] = [...subDirs]; 7 | subDirs.map((dir: string): void => { 8 | result.push(...getAllDirectoriesRecursivelyIn(dir)); 9 | }); 10 | 11 | return result; 12 | } 13 | 14 | export const getDirectoriesRecursivelyIn = (path: PathLike): FluentSyntaxForDirectoryFiltering => { 15 | const result = getAllDirectoriesRecursivelyIn(path); 16 | return new FluentSyntaxForDirectoryFiltering(result); 17 | }; 18 | -------------------------------------------------------------------------------- /steps/i-send-my-feedback-on-testcafe.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config.interface'; 2 | import { getCurrentConfig } from '../config/testcafe-config'; 3 | import * as selector from '../selectors'; 4 | import { t } from 'testcafe'; 5 | 6 | /** 7 | * @step 8 | * @when("I send my feedback on testcafe") 9 | */ 10 | export default async (): Promise => { 11 | // get the config that was injected into the fixture/test context by the feature 12 | const config: Config = getCurrentConfig(t); 13 | 14 | await t 15 | .setTestSpeed(config.testSpeed) 16 | .hover(selector.submitButton) 17 | .expect(selector.submitButton.hasAttribute('disabled')) 18 | .notOk({ timeout: config.timeout.longTimeout }) 19 | .click(selector.submitButton); 20 | }; 21 | -------------------------------------------------------------------------------- /tools/fs/get-func-name-from-file-name.ts: -------------------------------------------------------------------------------- 1 | export const getFuncNameFrom = (filename: string): string => { 2 | return filename 3 | .trim() 4 | .replace('.js', '') 5 | .replace('.ts', '') 6 | .trim() 7 | .replace(/\s/g, '-') 8 | .replace(/[^0-9a-zA-Z\-_.]/g, '') 9 | .split(/-|_|\./) 10 | .filter((word: string): boolean => (word && word.length > 0 ? true : false)) 11 | .map((word: string, wordIndex: number): string => { 12 | if (wordIndex === 0) { 13 | return word.toLocaleLowerCase(); 14 | } 15 | return [...word] 16 | .map((char, charIndex): string => { 17 | if (charIndex === 0) { 18 | return char.toUpperCase(); 19 | } 20 | return char; 21 | }) 22 | .join(''); 23 | }) 24 | .join(''); 25 | }; 26 | -------------------------------------------------------------------------------- /step-mappings/README.md: -------------------------------------------------------------------------------- 1 | # Step Mappings 2 | 3 | This folder is, by convention, the place for step-mappings files. 4 | 5 | step-mappings is the glue between the step-definition files and the feature files. 6 | 7 | step-mappings also provide strong type-checking and Visual Studio Code IntelliSense when writing steps in feature files. 8 | 9 | ![demo](../.media/demo3.gif) 10 | 11 | The steps mappings files are [steps.ts](steps.ts) and [index.ts](index.ts). 12 | 13 | These two files are auto-generated by a script located in the [generator](generator) folder. 14 | 15 | The generation process is driven by the configuration file [config.ts](config.ts) 16 | 17 | step-mappings files can be regenerated at any time by running the command: 18 | 19 | ```sh 20 | npm run build-step-mappings 21 | ``` 22 | 23 | > **You MUST run this command each time you add/modify/move/delete a step-definition file.** 24 | -------------------------------------------------------------------------------- /step-mappings/steps.ts: -------------------------------------------------------------------------------- 1 | // this file was auto-generated by 'generator/create-steps-barrel.ts' 2 | export { default as aXxxMessageShouldAppearWithMyName } from '../steps/a-xxx-message-should-appear-with-my-name'; 3 | export { default as iCanSubmitMyFeedbackOnTestcafe } from '../steps/i-can-submit-my-feedback-on-testcafe'; 4 | export { default as iCannotSubmitMyFeedbackOnTestcafe } from '../steps/i-cannot-submit-my-feedback-on-testcafe'; 5 | export { default as iDoSomethingSpecific } from '../steps/i-do-something-specific'; 6 | export { default as iEnterMyName } from '../steps/i-enter-my-name'; 7 | export { default as iNavigateToTheTestcafeSamplePage } from '../steps/i-navigate-to-the-testcafe-sample-page'; 8 | export { default as iSendMyFeedbackOnTestcafe } from '../steps/i-send-my-feedback-on-testcafe'; 9 | export { default as noNameShouldBePopulated } from '../steps/no-name-should-be-populated'; 10 | -------------------------------------------------------------------------------- /report-generator.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const report = require('multiple-cucumber-html-reporter'); 5 | 6 | const projectName = path.basename(__dirname); 7 | const projectVersion = process.env.npm_package_version; 8 | const reportGenerationTime = new Date().toISOString(); 9 | report.generate({ 10 | customData: { 11 | data: [ 12 | { label: 'Project', value: `${projectName}` }, 13 | { label: 'Release', value: `${projectVersion}` }, 14 | { label: 'Report Generation Time', value: `${reportGenerationTime}` }, 15 | ], 16 | title: 'Run info', 17 | }, 18 | disableLog: true, 19 | displayDuration: true, 20 | durationInMS: true, 21 | jsonDir: 'cucumber-json-reports', 22 | openReportInBrowser: true, 23 | reportName: 'TestCafe Report', 24 | reportPath: 'cucumber-json-reports/html', 25 | }); 26 | -------------------------------------------------------------------------------- /steps/i-navigate-to-the-testcafe-sample-page.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config.interface'; 2 | import { getCurrentConfig } from '../config/testcafe-config'; 3 | import { t } from 'testcafe'; 4 | 5 | /** 6 | * @step 7 | * @given,@when("I navigate to the testcafe sample page") 8 | */ 9 | export default async (): Promise => { 10 | // get the config that was injected into the fixture/test context by the feature 11 | const config: Config = getCurrentConfig(t); 12 | 13 | ensureEnvIsSetupInConfigurationFile(config); 14 | if (config && config.env) { 15 | await t.navigateTo(config.env.url); 16 | } 17 | }; 18 | 19 | function ensureEnvIsSetupInConfigurationFile(config: Config): void { 20 | if (config && config.env && config.env.url) { 21 | return; 22 | } 23 | throw new Error( 24 | 'env.url is not setup in the configuration file. Check testcafe-config.ts file is correctly setup' 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /step-mappings/config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const config: StepMappingConfig = { 4 | excludedFolders: [ 5 | // list of folders that should be excluded while searching all step files 6 | 'config', 7 | 'build', 8 | 'step-mappings', 9 | 'reports', 10 | 'step-snippets', 11 | 'step-templates', 12 | 'tools', 13 | ], 14 | quoteMark: "'", // quote character that should be used to generate import/export statements 15 | rootDirectory: process.cwd(), 16 | stepsBarrelFile: join('step-mappings', 'steps.ts'), // absolute path from rootDirectory 17 | stepsMappingFile: join('step-mappings', 'index.ts'), // absolute path from rootDirectory 18 | tab: ' ', // tab character that should be used to generate indented statements 19 | }; 20 | 21 | export interface StepMappingConfig { 22 | excludedFolders: string[]; 23 | rootDirectory: string; 24 | stepsBarrelFile: string; 25 | quoteMark: string; 26 | stepsMappingFile: string; 27 | tab: string; 28 | } 29 | -------------------------------------------------------------------------------- /steps/i-enter-my-name.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config.interface'; 2 | import { getCurrentConfig } from '../config/testcafe-config'; 3 | import { PageModel } from '../models'; 4 | import * as selector from '../selectors'; 5 | import { t } from 'testcafe'; 6 | 7 | /** 8 | * @step 9 | * @given,@when("I enter my name") 10 | */ 11 | export default async (): Promise => { 12 | // get the config that was injected into the fixture/test context by the feature 13 | const config: Config = getCurrentConfig(t); 14 | 15 | // get the page object model that was injected in the context 16 | const inputData = t.ctx.inputData as PageModel; 17 | 18 | const value = inputData.name || ''; 19 | 20 | await t 21 | .setTestSpeed(config.testSpeed) 22 | .hover(selector.userNameInputBox) 23 | .expect(selector.userNameInputBox.hasAttribute('disabled')) 24 | .notOk() 25 | .click(selector.userNameInputBox) 26 | .typeText(selector.userNameInputBox, value, { replace: true }) 27 | .pressKey('tab'); 28 | }; 29 | -------------------------------------------------------------------------------- /config/parsed-config.ts: -------------------------------------------------------------------------------- 1 | import { ParsedConfig } from './config.interface'; 2 | import { env } from './environments'; 3 | import { user } from './personas'; 4 | import * as minimist from 'minimist'; 5 | 6 | const args = minimist(process.argv.slice(2)); 7 | 8 | // get the options --env=xxx --user=yyy from the command line 9 | const config: ParsedConfig = { 10 | env: env(args.env), 11 | user: user(args.user), 12 | }; 13 | 14 | // get the option --testSpeed=xxx from the command line 15 | if (args && args.testSpeed) { 16 | config.testSpeed = Number(args.testSpeed); 17 | } 18 | config.timeout = {}; 19 | 20 | // get the option --longTimeout=xxx from the command line 21 | if (args && args.longTimeout) { 22 | config.timeout = { 23 | longTimeout: args.longTimeout, 24 | }; 25 | } 26 | 27 | // get the option --shortTimeout=xxx from the command line 28 | if (args && args.shortTimeout) { 29 | config.timeout = { 30 | ...config.timeout, 31 | shortTimeout: args.shortTimeout, 32 | }; 33 | } 34 | 35 | export const parsedConfig = config; 36 | -------------------------------------------------------------------------------- /step-filters/README.md: -------------------------------------------------------------------------------- 1 | # Step Filters 2 | 3 | This folder is, by convention, the place for step-filter files. 4 | 5 | A step-filter is a mechanism that enables to dynamically stop the execution of the current test. 6 | 7 | A test might be stopped because it is not running in the right environment, or because the persona setup in the configuration file must not run this test. 8 | 9 | This project offers you a filter mechanism for handling execution environment: see [env.ts](env.ts) and [this](../README.md#how-to-run-a-test-only-in-specific-environments). 10 | 11 | ## How it works 12 | 13 | A step-filter is technically the same as a given/when/then step. 14 | 15 | When a step-filter executes it should finish it's execution with either: 16 | 17 | ```js 18 | import {t} from "testcafe"; 19 | 20 | // code omitted for brevity 21 | 22 | t.ctx.canExecute = true; 23 | ``` 24 | or 25 | 26 | ```js 27 | import {t} from "testcafe"; 28 | 29 | // code omitted for brevity 30 | 31 | t.ctx.canExecute = false; // => all steps executed after this line are skipped 32 | ``` 33 | -------------------------------------------------------------------------------- /tools/fs/get-directories-in.ts: -------------------------------------------------------------------------------- 1 | import { isDirectory } from './is-directory'; 2 | import { PathLike, readdirSync } from 'fs'; 3 | import { join } from 'path'; 4 | export class FluentSyntaxForDirectoryFiltering { 5 | private allResults: string[] = []; 6 | private filteredResults: string[] = []; 7 | 8 | public constructor(paths: string[]) { 9 | this.allResults = [...paths]; 10 | this.filteredResults = [...paths]; 11 | } 12 | public takeAll(): string[] { 13 | return this.allResults; 14 | } 15 | public takeFiltered(): string[] { 16 | return this.filteredResults; 17 | } 18 | public withFilter(filter: (path: string) => boolean): FluentSyntaxForDirectoryFiltering { 19 | this.filteredResults = [...this.filteredResults].filter(filter); 20 | return this; 21 | } 22 | } 23 | 24 | export const getDirectoriesIn = (path: PathLike): FluentSyntaxForDirectoryFiltering => { 25 | const result = readdirSync(path) 26 | .map((name: string): string => join(path.toString(), name)) 27 | .filter(isDirectory); 28 | 29 | return new FluentSyntaxForDirectoryFiltering(result); 30 | }; 31 | -------------------------------------------------------------------------------- /tools/fs/get-exported-functions-in.ts: -------------------------------------------------------------------------------- 1 | import { readAllLinesInFile } from './read-all-lines-in-file'; 2 | import { PathLike } from 'fs'; 3 | 4 | export const getExportedFunctionsIn = (filePath: PathLike): FuncInfo[] => { 5 | try { 6 | const results: FuncInfo[] = []; 7 | const lines = readAllLinesInFile(filePath); 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | const module = require(filePath.toString()); 10 | for (const key in module) { 11 | if (Object.prototype.hasOwnProperty.call(module, key) && typeof module[key] === 'function') { 12 | const lineNumber = 13 | 1 + 14 | lines.findIndex( 15 | (line: string): boolean => line.includes('export ') && line.includes(key) 16 | ); 17 | const functionName = key; 18 | results.push({ functionName, lineNumber, filePath }); 19 | } 20 | } 21 | return results; 22 | } catch (error) { 23 | return []; 24 | } 25 | }; 26 | 27 | export interface FuncInfo { 28 | functionName: string; 29 | lineNumber: number; 30 | filePath: PathLike; 31 | } 32 | -------------------------------------------------------------------------------- /tools/fs/get-relative-path-from.ts: -------------------------------------------------------------------------------- 1 | import { getFileName } from './get-filename'; 2 | import { isFile } from './is-file'; 3 | import { dirname, relative, sep } from 'path'; 4 | 5 | export const getRelativePathOf = ( 6 | fileOrFolderPath: string 7 | ): { from: (originFileOrFolderPath: string) => string } => { 8 | /* prettier-ignore */ 9 | const toFolder: string = isFile(fileOrFolderPath) 10 | ? dirname(fileOrFolderPath) 11 | : fileOrFolderPath; 12 | 13 | /* prettier-ignore */ 14 | const toFilename = isFile(fileOrFolderPath) 15 | ? getFileName(fileOrFolderPath) 16 | : undefined; 17 | 18 | return { 19 | from: (originFileOrFolderPath: string): string => { 20 | const fromFolder: string = isFile(originFileOrFolderPath) 21 | ? dirname(originFileOrFolderPath) 22 | : originFileOrFolderPath; 23 | 24 | const relativeFolderPath = relative(fromFolder, toFolder) || '.'; 25 | 26 | /* prettier-ignore */ 27 | const result = toFilename 28 | ? `${relativeFolderPath}${sep}${toFilename}` 29 | : relativeFolderPath; 30 | 31 | return result; 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Henri d'Orgeval 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | addons: 3 | chrome: stable 4 | apt: 5 | packages: 6 | - libnss3 7 | # These are required to run webkit 8 | - libwoff1 9 | - libopus0 10 | - libwebp6 11 | - libwebpdemux2 12 | - libenchant1c2a 13 | - libgudev-1.0-0 14 | - libsecret-1-0 15 | - libhyphen0 16 | - libgdk-pixbuf2.0-0 17 | - libegl1 18 | - libgles2 19 | - libevent-2.1-6 20 | - libnotify4 21 | - libxslt1.1 22 | - libvpx5 23 | # This is required to run chromium 24 | - libgbm1 25 | # this is needed for running headful tests 26 | - xvfb 27 | 28 | language: node_js 29 | sudo: false 30 | 31 | node_js: 32 | - '12' 33 | 34 | before_install: 35 | # Info about OS 36 | - uname 37 | - if [[ `node -v` = v6* ]]; then npm i -g npx; fi 38 | - npx --version 39 | # Launch XVFB 40 | - if [[ `uname` = "Linux" ]]; then export DISPLAY=:99.0; fi 41 | 42 | script: 43 | - node --version 44 | - npm --version 45 | - npx --version 46 | - npm run build 47 | - if [[ `uname` = "Linux" ]]; then xvfb-run --auto-servernum npm test; else npm test; fi 48 | 49 | os: 50 | - linux 51 | # - osx 52 | # osx_image: xcode11.3 53 | -------------------------------------------------------------------------------- /steps/a-xxx-message-should-appear-with-my-name.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentConfig } from '..//config/testcafe-config'; 2 | import { PageModel } from '../models'; 3 | import * as selector from '../selectors'; 4 | import { firstMatch } from '../tools/regex-match'; 5 | import { t } from 'testcafe'; 6 | 7 | /** 8 | * @step 9 | * @then("a 'Thank you' message should appear with my name") 10 | */ 11 | export default async (stepName: string): Promise => { 12 | // get the page object model that was injected in the context 13 | const inputData = t.ctx.inputData as PageModel; 14 | const config = getCurrentConfig(t); 15 | 16 | // extract the message embedded in the step name 17 | // by convention this value is prefixed and postfixed by a single quote 18 | const message = firstMatch(/'.*'/g, stepName); 19 | if (message === null) { 20 | throw new Error(`Cannot extract message from the step name "${stepName}"`); 21 | } 22 | 23 | const myName = inputData.name || ''; 24 | await t 25 | .expect(selector.resultContent.exists) 26 | .ok({ timeout: config.timeout.longTimeout }) 27 | .expect(selector.resultContent.innerText) 28 | .contains(message) 29 | .expect(selector.resultContent.innerText) 30 | .contains(myName); 31 | }; 32 | -------------------------------------------------------------------------------- /config/personas.ts: -------------------------------------------------------------------------------- 1 | export const personas: UserInfo[] = [ 2 | { name: 'user 1', login: 'user1@example.com', password: 'user1' }, 3 | { name: 'user 2', login: 'user2@example.com', password: 'user2' }, 4 | { name: 'user 3', login: 'user3@example.com', password: 'user3' }, 5 | ]; 6 | 7 | const userLogins: string[] = personas.map((p: UserInfo): string => p.login); 8 | 9 | export function user(login: Email | undefined): UserInfo | undefined { 10 | if (login === undefined) { 11 | // eslint-disable-next-line no-console 12 | console.warn(`User login is undefined. Available logins are: ${userLogins}. 13 | (You can optionnaly add to the testcafe command-line the option: --user=${userLogins[0]})`); 14 | return undefined; 15 | } 16 | const foundUser = personas.filter((p: UserInfo): boolean => p.login === login)[0]; 17 | if (foundUser) { 18 | return foundUser; 19 | } 20 | 21 | // eslint-disable-next-line no-console 22 | console.warn(`User login "${login}" is not found. Available logins are: ${userLogins}`); 23 | return undefined; 24 | } 25 | export interface UserInfo { 26 | name: string; 27 | login: Email; 28 | password?: string; 29 | } 30 | 31 | export type Email = 'user1@example.com' | 'user2@example.com' | 'user3@example.com'; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Screenshots 2 | screenshots 3 | 4 | #reports 5 | reports 6 | cucumber-json-reports 7 | 8 | #build 9 | build 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Typescript v1 declaration files 50 | typings/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | 70 | -------------------------------------------------------------------------------- /features/testcafe-sample-page.spec.ts: -------------------------------------------------------------------------------- 1 | import 'testcafe'; 2 | import { getCurrentConfig } from '../config/testcafe-config'; 3 | import { pageModel } from '../models'; 4 | import { env } from '../step-filters/env'; 5 | import { and, given, then, when } from '../step-runner'; 6 | declare const fixture: FixtureFn; 7 | /** 8 | * @feature 9 | */ 10 | fixture('Feature: TestCafe Example') 11 | .before(async (ctx) => { 12 | // inject global configuration in the fixture context 13 | ctx.config = getCurrentConfig(); 14 | }) 15 | .beforeEach(async (t) => { 16 | // inject page model in the test context 17 | t.ctx.inputData = pageModel; 18 | await given('I navigate to the testcafe sample page'); 19 | }); 20 | 21 | test('Scenario: cannot submit my feedback when I did not enter my name', async () => { 22 | await then('no name should be populated'); 23 | await and('I cannot submit my feedback on testcafe'); 24 | }); 25 | 26 | test('Scenario: can send feedback with my name only', async () => { 27 | await when('I enter my name'); 28 | await then('I can submit my feedback on testcafe'); 29 | }); 30 | 31 | test('Scenario: send feedback', async () => { 32 | await env.only('devci'); 33 | await given('I enter my name'); 34 | await when('I send my feedback on testcafe'); 35 | await then("a 'Thank you' message should appear with my name"); 36 | }); 37 | -------------------------------------------------------------------------------- /step-mappings/index.ts: -------------------------------------------------------------------------------- 1 | // this file was auto-generated by 'generator/create-steps-mappings.ts' 2 | import * as step from './steps'; 3 | 4 | export interface StepMappings { 5 | [index: string]: (stepname: string) => Promise; 6 | } 7 | export const givenStepMappings = { 8 | 'I enter my name': step.iEnterMyName, 9 | 'I navigate to the testcafe sample page': step.iNavigateToTheTestcafeSamplePage, 10 | }; 11 | export type GivenStep = keyof typeof givenStepMappings; 12 | export const whenStepMappings = { 13 | 'I do something specific': step.iDoSomethingSpecific, 14 | 'I enter my name': step.iEnterMyName, 15 | 'I navigate to the testcafe sample page': step.iNavigateToTheTestcafeSamplePage, 16 | 'I send my feedback on testcafe': step.iSendMyFeedbackOnTestcafe, 17 | }; 18 | export type WhenStep = keyof typeof whenStepMappings; 19 | export const thenStepMappings = { 20 | "a 'Thank you' message should appear with my name": step.aXxxMessageShouldAppearWithMyName, 21 | 'I can submit my feedback on testcafe': step.iCanSubmitMyFeedbackOnTestcafe, 22 | 'I cannot submit my feedback on testcafe': step.iCannotSubmitMyFeedbackOnTestcafe, 23 | 'no name should be populated': step.noNameShouldBePopulated, 24 | }; 25 | export type ThenStep = keyof typeof thenStepMappings; 26 | export const butStepMappings = {}; 27 | export type ButStep = keyof typeof butStepMappings; 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.useTabStops": true, 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/CVS": true, 9 | "**/.DS_Store": true, 10 | "**/build": true, 11 | "node_modules": true 12 | }, 13 | "editor.tabCompletion": "on", 14 | "editor.formatOnPaste": true, 15 | "editor.formatOnSave": true, 16 | "editor.minimap.enabled": true, 17 | "editor.minimap.showSlider": "always", 18 | "editor.minimap.renderCharacters": true, 19 | "editor.mouseWheelZoom": true, 20 | "search.exclude": { 21 | "**/node_modules": true, 22 | "**/bower_components": true 23 | }, 24 | "javascript.referencesCodeLens.enabled": true, 25 | "javascript.suggest.completeFunctionCalls": true, 26 | "typescript.referencesCodeLens.enabled": true, 27 | "typescript.implementationsCodeLens.enabled": true, 28 | "typescript.suggest.completeFunctionCalls": true, 29 | "typescript.tsdk": "./node_modules/typescript/lib", 30 | "prettier.singleQuote": true, 31 | "prettier.trailingComma": "all", 32 | "eslint.alwaysShowStatus": true, 33 | "eslint.options": { 34 | "extensions": [".js", ".ts", ".tsx"] 35 | }, 36 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 37 | "editor.codeActionsOnSave": { 38 | "source.fixAll.eslint": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/environments.ts: -------------------------------------------------------------------------------- 1 | export const environments: EnvInfo[] = [ 2 | { name: 'local', url: 'http://devexpress.github.io/testcafe/example' }, 3 | { name: 'devci', url: 'http://devci.my-company.com' }, 4 | { name: 'uat1', url: 'http://uat1.my-company.com' }, 5 | { name: 'uat2', url: 'http://uat2.my-company.com' }, 6 | { name: 'prod', url: 'http://prod.my-company.com' }, 7 | ]; 8 | 9 | const envNames: string[] = environments.map((e: EnvInfo): string => e.name); 10 | 11 | export function env(name: TargetEnvironnement | undefined): EnvInfo | undefined { 12 | if (name === undefined) { 13 | // eslint-disable-next-line no-console 14 | console.warn(`Environment name is undefined. Available environments are: ${envNames}. 15 | (You can optionnaly add to the testcafe command-line the option: --env=${envNames[0]})`); 16 | return undefined; 17 | } 18 | const foundEnvironment = environments.filter((e: EnvInfo): boolean => e.name === name)[0]; 19 | if (foundEnvironment) { 20 | return foundEnvironment; 21 | } 22 | 23 | // eslint-disable-next-line no-console 24 | console.warn(`Environment "${name}" is not found. Available environments are: ${envNames}`); 25 | return undefined; 26 | } 27 | 28 | export interface EnvInfo { 29 | name: TargetEnvironnement; 30 | url: string; 31 | } 32 | export type TargetEnvironnement = 'any' | 'local' | 'devci' | 'uat1' | 'uat2' | 'prod'; 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "jest": true, 7 | "jasmine": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier/@typescript-eslint", 13 | "plugin:prettier/recommended", 14 | "plugin:import/errors", 15 | "plugin:import/warnings", 16 | "plugin:import/typescript", 17 | "plugin:testcafe/recommended" 18 | ], 19 | "globals": { 20 | "Atomics": "readonly", 21 | "SharedArrayBuffer": "readonly" 22 | }, 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "ecmaVersion": 2018, 26 | "sourceType": "module", 27 | "ecmaFeatures": { 28 | "modules": true 29 | } 30 | }, 31 | "plugins": ["@typescript-eslint", "prettier", "import", "testcafe"], 32 | "rules": { 33 | "@typescript-eslint/explicit-function-return-type": "off", 34 | "indent": ["error", 2, { 35 | "SwitchCase": 1 36 | }], 37 | "no-console": ["error"], 38 | "no-debugger": ["error"], 39 | "no-multiple-empty-lines": [ 40 | "error", 41 | { 42 | "max": 1, 43 | "maxEOF": 1 44 | } 45 | ], 46 | "semi": ["error", "always"], 47 | "import/order": ["error", { 48 | "groups": ["index", "sibling", "parent", "internal", "external", "builtin"] 49 | }] 50 | } 51 | } -------------------------------------------------------------------------------- /step-filters/env.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config.interface'; 2 | import { TargetEnvironnement } from '../config/environments'; 3 | import { getCurrentConfig } from '../config/testcafe-config'; 4 | import { t } from 'testcafe'; 5 | import * as chalk from 'chalk'; 6 | 7 | export const env: { only: (...targets: TargetEnvironnement[]) => void } = { 8 | only: async (...targets: TargetEnvironnement[]): Promise => { 9 | const config: Config = getCurrentConfig(t); 10 | if (config.env === undefined) { 11 | throw new Error('The env property in the configuration file is not defined.'); 12 | } 13 | if (config.env.name === 'any') { 14 | // filters are bypassed if the environment in the configuration file is any 15 | t.ctx.canExecute = true; 16 | return; 17 | } 18 | if (targets.length === 0) { 19 | return; 20 | } 21 | t.ctx.canExecute = false; 22 | for (const target of targets) { 23 | if (config.env.name === target) { 24 | t.ctx.canExecute = true; 25 | } 26 | if (target === 'any') { 27 | t.ctx.canExecute = true; 28 | } 29 | } 30 | if (t.ctx.canExecute === false) { 31 | const message = `next steps will not run because scenario is targeted only for ${targets}`; 32 | 33 | // eslint-disable-next-line no-console 34 | console.log(` ${chalk.inverse(message)}`); 35 | } 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "name": "Current TS File", 8 | "type": "node", 9 | "request": "launch", 10 | "args": ["${relativeFile}"], 11 | "runtimeArgs": ["-r", "ts-node/register"], 12 | "cwd": "${workspaceRoot}", 13 | "protocol": "inspector", 14 | "internalConsoleOptions": "openOnSessionStart" 15 | }, 16 | { 17 | "type": "node", 18 | "protocol": "inspector", 19 | "request": "launch", 20 | "name": "TestCafe", 21 | "program": "${workspaceRoot}/node_modules/testcafe/bin/testcafe.js", 22 | "args": [ 23 | "chrome", 24 | "features/**/*.spec.ts", 25 | "--debug-on-fail", 26 | "--selector-timeout 10000" 27 | // setup the config/default-config.ts file before debugging 28 | ], 29 | "cwd": "${workspaceRoot}" 30 | }, 31 | { 32 | "type": "node", 33 | "protocol": "inspector", 34 | "request": "launch", 35 | "name": "testcafe-static-analyser", 36 | "program": "${workspaceRoot}/node_modules/testcafe-static-analyser/bin/testcafe-static-analyser.js", 37 | "args": [], 38 | "cwd": "${workspaceRoot}" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /step-mappings/generator/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { createStepsBarrel } from './create-steps-barrel'; 3 | import { createStepsMapping } from './create-steps-mappings'; 4 | import { getDirectoriesRecursivelyIn } from '../../tools/fs/get-directories-recursively-in'; 5 | import { getFilesInDirectory } from '../../tools/fs/get-files-in-directory'; 6 | import { ignoreDirThatIsIn } from '../../tools/fs/ignore-dir-that-is-in'; 7 | import { ignoreDotDir } from '../../tools/fs/ignore-dot-dir'; 8 | import { ignoreNodeModules } from '../../tools/fs/ignore-node-modules'; 9 | import { isStepFile } from '../../tools/fs/is-step-file'; 10 | import { config } from '../config'; 11 | 12 | const stepFiles: string[] = []; 13 | const folders = getDirectoriesRecursivelyIn(config.rootDirectory) 14 | .withFilter(ignoreDotDir) 15 | .withFilter(ignoreNodeModules) 16 | .withFilter(ignoreDirThatIsIn(config.excludedFolders)) 17 | .takeFiltered(); 18 | 19 | folders.forEach((path: string): number => 20 | stepFiles.push( 21 | ...getFilesInDirectory(path, (p: string): boolean => p.endsWith('.ts') && isStepFile(p)) 22 | ) 23 | ); 24 | 25 | stepFiles.length > 0 26 | ? console.log(`[build-step-mappings] found step files: \n${stepFiles.join('\n')}`) 27 | : console.log('[build-step-mappings] no step found'); 28 | 29 | createStepsBarrel(config.stepsBarrelFile).from(stepFiles); 30 | 31 | createStepsMapping(config.stepsMappingFile).from(stepFiles); 32 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # TestCafe Config 2 | 3 | This folder is, by convention, the place for all configuration files used at runtime by every steps. 4 | 5 | ## Environment configuration 6 | You can target the tests execution for any kind of environment. All available environments should be declared in the [environments.ts](environments.ts) file. 7 | 8 | ## Persona configuration 9 | You can target the tests execution for any kind of users. All available users should be declared in the [personas.ts](personas.ts) file. 10 | 11 | ## Custom command-line options 12 | You can add any custom command-line options to the existing TestCafe command-line options. To do this, customize the content of [parsed-config.ts](parsed-config.ts). 13 | 14 | ## Usage 15 | 16 | The configuration object is injected in tests via the before hook of the fixture statement: 17 | 18 | ```js 19 | fixture("Feature: TestCafe Example") 20 | .before(async (ctx) => { 21 | // inject global configuration in the fixture context 22 | ctx.config = getCurrentConfig(); 23 | }); 24 | ``` 25 | 26 | The configuration object is then retrieved by each step with the following statements: 27 | 28 | ```js 29 | import {t} from "testcafe"; 30 | 31 | // code omitted for brevity 32 | 33 | // get the config that was injected into the fixture/test context by the feature 34 | const config = getCurrentConfig(t); 35 | await t 36 | .expect(selector.submitButton.hasAttribute("disabled")) 37 | .notOk({timeout: config.timeout.longTimeout}); 38 | ``` 39 | -------------------------------------------------------------------------------- /config/testcafe-config.ts: -------------------------------------------------------------------------------- 1 | import 'testcafe'; 2 | import { Config } from './config.interface'; 3 | import { defaultConfig } from './default-config'; 4 | import { parsedConfig } from './parsed-config'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const jsome = require('jsome'); 8 | 9 | const config: Config = { 10 | ...defaultConfig, 11 | }; 12 | 13 | if (parsedConfig && parsedConfig.env) { 14 | config.env = parsedConfig.env; 15 | } 16 | 17 | if (parsedConfig && parsedConfig.user) { 18 | config.user = parsedConfig.user; 19 | } 20 | if (parsedConfig && parsedConfig.testSpeed) { 21 | config.testSpeed = parsedConfig.testSpeed; 22 | } 23 | 24 | if (parsedConfig && parsedConfig.timeout && parsedConfig.timeout.longTimeout) { 25 | config.timeout.longTimeout = parsedConfig.timeout.longTimeout; 26 | } 27 | if (parsedConfig && parsedConfig.timeout && parsedConfig.timeout.shortTimeout) { 28 | config.timeout.shortTimeout = parsedConfig.timeout.shortTimeout; 29 | } 30 | 31 | if (config.showConfig) { 32 | // eslint-disable-next-line no-console 33 | console.log('Tests will run with the following global configuration: '); 34 | jsome(config); 35 | } 36 | 37 | export default config; 38 | 39 | export function getCurrentConfig(t?: TestController): Config { 40 | if (t && t.ctx && t.ctx.config) { 41 | return t.ctx.config; 42 | } 43 | 44 | if (t && t.fixtureCtx && t.fixtureCtx.config) { 45 | return t.fixtureCtx.config; 46 | } 47 | 48 | return JSON.parse(JSON.stringify(config)); 49 | } 50 | -------------------------------------------------------------------------------- /tools/fs/get-jsdoc-of-function.ts: -------------------------------------------------------------------------------- 1 | import { FuncInfo } from './get-exported-functions-in'; 2 | import { readAllLinesInFile } from './read-all-lines-in-file'; 3 | 4 | const LAST_LINE_OF_JSDOC = '*/'; 5 | const FIRST_LINE_OF_JSODC = '/**'; 6 | export const getJsDocCommentsOf = (funcInfo: FuncInfo): string[] => { 7 | const lineNumberOfFunctionDeclaration = funcInfo.lineNumber - 1; 8 | if (lineNumberOfFunctionDeclaration <= 0) { 9 | return []; 10 | } 11 | const allLines = readAllLinesInFile(funcInfo.filePath); 12 | if (lineNumberOfFunctionDeclaration > allLines.length - 1) { 13 | return []; 14 | } 15 | const functionDaclarationLine = allLines[lineNumberOfFunctionDeclaration]; 16 | if ( 17 | (functionDaclarationLine.includes('export') && 18 | functionDaclarationLine.includes(funcInfo.functionName)) === false 19 | ) { 20 | return []; 21 | } 22 | 23 | const lines = allLines 24 | .filter((_, index): boolean => index < lineNumberOfFunctionDeclaration) 25 | .map((line: string): string => line.trim()) 26 | .filter((line: string): boolean => line.length > 0) 27 | .filter((line: string): boolean => line.startsWith('//') === false); 28 | 29 | const firstLineAboveFunctionDeclaration = lines.pop(); 30 | if (firstLineAboveFunctionDeclaration !== LAST_LINE_OF_JSDOC) { 31 | return []; 32 | } 33 | 34 | const jsDocComments: string[] = []; 35 | for (let index = lines.length - 1; index >= 0; index--) { 36 | const line = lines[index]; 37 | if (line === FIRST_LINE_OF_JSODC) { 38 | break; 39 | } 40 | jsDocComments.unshift(line); 41 | } 42 | 43 | return jsDocComments; 44 | }; 45 | -------------------------------------------------------------------------------- /features/README.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | This folder is, by convention, the place for the TestCafe feature files. 4 | 5 | ## Creating a new feature file 6 | 7 | - Create an empty TypeScript file and give it a name like `my-new-feature.spec.ts` 8 | 9 | - Paste the following code in this new file: 10 | 11 | ```typescript 12 | import 'testcafe'; 13 | import { getCurrentConfig } from '../config/testcafe-config'; 14 | import { pageModel } from '../domains/my-app'; 15 | import { env } from '../step-filters/env'; 16 | import { and, given, then, when } from '../step-runner'; 17 | 18 | fixture(`Feature: my new feature`) 19 | .before(async (ctx) => { 20 | // inject global configuration in the fixture context 21 | ctx.config = getCurrentConfig(); 22 | }) 23 | .beforeEach(async (t) => { 24 | // inject page model in the test context 25 | t.ctx.inputData = pageModel; 26 | }); 27 | 28 | test('Scenario: my new scenario', async () => { 29 | await given('I start my App'); 30 | await and('I input the given data'); 31 | await when('I send my form'); 32 | await then('I should receive a specific response'); 33 | }); 34 | ``` 35 | 36 | This starter project has been designed to put the business at the center of the e2e strategy. 37 | 38 | The way you express the different steps should be aligned with the language used by the business and/or end-users. 39 | Take time to make your tests readable by those people. 40 | 41 | Visual Studio Code IntelliSense signals you these new steps are unknown: 42 | 43 | ![unknown steps](../.media/screenshot01.png) 44 | 45 | To see if an existing step can be used, just empty the step name and use VS Code IntelliSense: 46 | 47 | ![unknown steps](../.media/screenshot02.png) 48 | 49 | If you don't find an existing suitable step, you need to create a new one [here](../steps/README.md). 50 | -------------------------------------------------------------------------------- /step-templates/basic-template-step.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config.interface'; 2 | import { getCurrentConfig } from '../config/testcafe-config'; 3 | import { PageModel } from '../models'; 4 | import * as selector from '../selectors'; 5 | import { firstMatch } from '../tools/regex-match'; 6 | import { t } from 'testcafe'; 7 | 8 | /** 9 | * @step 10 | * @given("I do something") 11 | */ 12 | export default async (stepName: string): Promise => { 13 | // get the config that was injected into the fixture context by the feature 14 | const config: Config = getCurrentConfig(t); 15 | 16 | // get the page object model that was injected in the test context 17 | const inputData = t.ctx.inputData as PageModel; 18 | 19 | // extract the value embedded in the step name 20 | // by convention this value is prefixed and postfixed by a single quote 21 | const value = firstMatch(/'.*'/g, stepName); 22 | if (value === null) { 23 | throw new Error(`Cannot extract value from the step name "${stepName}"`); 24 | } 25 | 26 | // you may use the Visual Studio Code Extension Testcafe Snippets 27 | // to help you write your tests 28 | 29 | await t 30 | .setTestSpeed(config.testSpeed) 31 | .hover(selector.firstInputBox) 32 | .expect(selector.firstInputBox.hasAttribute('disabled')) 33 | .notOk({ timeout: config.timeout.longTimeout }) 34 | .typeText(selector.firstInputBox, value, { replace: true }) 35 | .pressKey('tab'); 36 | 37 | if (inputData.name) { 38 | await t 39 | .setTestSpeed(config.testSpeed) 40 | .hover(selector.secondInputBox) 41 | .expect(selector.secondInputBox.hasAttribute('disabled')) 42 | .notOk({ timeout: config.timeout.longTimeout }) 43 | .typeText(selector.secondInputBox, inputData.name, { replace: true }) 44 | .pressKey('tab'); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /steps/i-do-something-specific.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config.interface'; 2 | import { getCurrentConfig } from '../config/testcafe-config'; 3 | import { PageModel } from '../models'; 4 | import * as selector from '../selectors'; 5 | import { firstMatch } from '../tools/regex-match'; 6 | import { t } from 'testcafe'; 7 | 8 | /** 9 | * @step 10 | * @when("I do something specific") 11 | */ 12 | export default async (stepName: string): Promise => { 13 | // get the config that was injected into the fixture context by the feature 14 | const config: Config = getCurrentConfig(t); 15 | 16 | // get the page object model that was injected in the test context 17 | const inputData = t.ctx.inputData as PageModel; 18 | 19 | // extract the value embedded in the step name 20 | // by convention this value is prefixed and postfixed by a single quote 21 | const value = firstMatch(/'.*'/g, stepName); 22 | if (value === null) { 23 | throw new Error(`Cannot extract value from the step name "${stepName}"`); 24 | } 25 | 26 | // you may use the Visual Studio Code Extension Testcafe Snippets 27 | // to help you write your tests 28 | 29 | await t 30 | .setTestSpeed(config.testSpeed) 31 | .hover(selector.firstInputBox) 32 | .expect(selector.firstInputBox.hasAttribute('disabled')) 33 | .notOk({ timeout: config.timeout.longTimeout }) 34 | .typeText(selector.firstInputBox, value, { replace: true }) 35 | .pressKey('tab'); 36 | 37 | if (inputData.name) { 38 | await t 39 | .setTestSpeed(config.testSpeed) 40 | .hover(selector.secondInputBox) 41 | .expect(selector.secondInputBox.hasAttribute('disabled')) 42 | .notOk({ timeout: config.timeout.longTimeout }) 43 | .typeText(selector.secondInputBox, inputData.name, { replace: true }) 44 | .pressKey('tab'); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /step-mappings/generator/create-steps-barrel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | import { ensureDirectoryStructureExists } from '../../tools/fs/ensure-directory-structure-exists'; 3 | import { getFileName } from '../../tools/fs/get-filename'; 4 | import { getFilePathWithoutExtension } from '../../tools/fs/get-filepath-without-extension'; 5 | import { getFuncNameFrom } from '../../tools/fs/get-func-name-from-file-name'; 6 | import { getRelativePathOf } from '../../tools/fs/get-relative-path-from'; 7 | import { slash } from '../../tools/fs/slash'; 8 | import { config } from '../config'; 9 | import { EOL } from 'os'; 10 | import { PathLike, writeFileSync } from 'fs'; 11 | 12 | let exportIndex = -1; 13 | function nextIndex(): number { 14 | exportIndex += 1; 15 | return exportIndex; 16 | } 17 | export const createStepsBarrel = ( 18 | barrelFilePath: PathLike 19 | ): { from: (stepFiles: string[]) => void } => { 20 | ensureDirectoryStructureExists(barrelFilePath.toString()); 21 | writeFileSync(barrelFilePath, 'searching steps ...'); 22 | return { 23 | from: (stepFiles: string[]): void => { 24 | const lines: string[] = []; 25 | lines.push( 26 | `// this file was auto-generated by '${getRelativePathOf(__filename).from( 27 | config.stepsBarrelFile 28 | )}'` 29 | ); 30 | stepFiles.forEach((filePath: string): void => { 31 | const fileName = getFileName(filePath) || `defaultStep${nextIndex()}`; 32 | const relativePath = getRelativePathOf(filePath).from(barrelFilePath.toString()); 33 | 34 | const defaultExportName = getFuncNameFrom(fileName); 35 | // eslint-disable-next-line @typescript-eslint/no-var-requires 36 | const module = require(filePath); 37 | 38 | module.default 39 | ? lines.push( 40 | `export { default as ${defaultExportName} } from ${config.quoteMark}${slash( 41 | getFilePathWithoutExtension(relativePath) 42 | )}${config.quoteMark};` 43 | ) 44 | : lines.push( 45 | `export * from ${config.quoteMark}${slash( 46 | getFilePathWithoutExtension(relativePath) 47 | )}${config.quoteMark};` 48 | ); 49 | }); 50 | lines.push(''); 51 | 52 | writeFileSync(barrelFilePath, lines.join(EOL)); 53 | }, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /steps/README.md: -------------------------------------------------------------------------------- 1 | # TestCafe Step-definition files 2 | 3 | This folder is, by convention, the place for all step definitions. 4 | 5 | ## Creating a new step 6 | 7 | Let's say you want to create a step-definition file for the sentence `When I do something specific`. 8 | 9 | - Create a new empty file in the `steps` folder and name it `i-do-something-specific.ts`. 10 | 11 | - Consider using the kebab-case naming convention in order to quickly find the implementation associated to the step through the Command Palette: 12 | 13 | ![find the step implementation](../.media/screenshot05.png) 14 | 15 | - Copy, in this new file, the content of the [basic template step definition file](../step-templates/basic-template-step.ts) or the content of the [not implemented step definition file](../step-templates/not-implemented-step.ts); 16 | 17 | - Adapt the jsDoc comments: 18 | 19 | ```js 20 | /** 21 | * @step 22 | * @when("I do something specific") 23 | */ 24 | ``` 25 | 26 | - If the statement should be available as a `Given` **and** a `When` step, the JSDoc comments should be: 27 | 28 | ```js 29 | /** 30 | * @step 31 | * @given,@when("I do something specific") 32 | */ 33 | ``` 34 | 35 | - If the same step-definition must be re-used for different statements, the JSDoc comments should be (see [this step implementation](a-xxx-message-should-appear-with-my-name.ts) for more details): 36 | 37 | ```js 38 | /** 39 | * @step 40 | * @given,@when("I do something specific") 41 | * @when("I set the user name as 'john doe'") 42 | * @when("I set the user name as 'Perry Scope'") 43 | * @when("I set the user name as 'Art Decco'") 44 | */ 45 | ``` 46 | 47 | - for a `Then` step: 48 | 49 | ```js 50 | /** 51 | * @step 52 | * @then("I do something specific") 53 | */ 54 | ``` 55 | 56 | - for a `But` step: 57 | 58 | ```js 59 | /** 60 | * @step 61 | * @but("I do something specific") 62 | */ 63 | ``` 64 | 65 | - Save the new step-definition file and run the following command in a terminal window: 66 | 67 | ```sh 68 | npm run build-step-mappings 69 | ``` 70 | 71 | - Now go back to the feature file. The sentence should be available in the intellisense: 72 | 73 | ![demo](../.media/screenshot09.png) 74 | 75 | ## Modifying/moving/deleting a new step 76 | 77 | > **You MUST run this command each time you add/modify/move/delete a step-definition file.**: 78 | 79 | ```sh 80 | npm run build-step-mappings 81 | ``` 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [7.4.1] - 2020-07-14 9 | 10 | ### Fixed 11 | 12 | - update TestCafé reporter `testcafe-reporter-cucumber-json` 13 | - update dependencies 14 | 15 | ## [7.4.0] - 2020-07-11 16 | 17 | ### Changed 18 | 19 | - update all dependencies 20 | - fix vulnerabilities 21 | 22 | ## [7.3.0] - 2020-05-04 23 | 24 | ### Changed 25 | 26 | - update all dependencies 27 | - fix vulnerabilities 28 | 29 | ## [7.2.0] - 2019-12-01 30 | 31 | ### Changed 32 | 33 | - update all dependencies 34 | 35 | ## [7.1.0] - 2019-11-23 36 | 37 | ### Changed 38 | 39 | - update all dependencies 40 | - fix vulnerabilities 41 | 42 | ## [7.0.0] - 2019-11-16 43 | 44 | ### Changed 45 | 46 | - update all dependencies 47 | - fix vulnerabilities 48 | - update eslint configuration 49 | 50 | ## [6.0.0] - 2019-08-11 51 | 52 | ### Changed 53 | 54 | - update all dependencies 55 | - fix vulnerabilities 56 | 57 | ## [5.0.0] - 2019-07-30 58 | 59 | ### Changed 60 | 61 | - update all dependencies 62 | - replace tslint by eslint 63 | 64 | ## [4.2.0] - 2019-06-12 65 | 66 | ### Changed 67 | 68 | - update all dependencies 69 | 70 | ## [4.1.0] - 2019-03-24 71 | 72 | ### Changed 73 | 74 | - update all dependencies 75 | 76 | ## [4.0.0] - 2019-03-11 77 | 78 | ### Changed 79 | 80 | - adapt this starter project to new TestCafe v1.0.0 81 | - integrate HTML reporter 82 | - update all dependencies 83 | - remove TestCafe Live dependency (now integrated in TestCafe) 84 | - simplify project structure 85 | - remove static analyser 86 | 87 | ## [3.1.1] - 2018-12-18 88 | 89 | ### Changed 90 | 91 | - update testcafe-static-analyser dependency. 92 | - this new version is able to analyse `.meta` syntax and `test`syntax split on multiple lines. 93 | 94 | ## [3.1.0] - 2018-12-07 95 | 96 | ### Fixed 97 | 98 | - update dependencies and remove vulnerability on 'event-stream' 99 | 100 | ## [3.0.0] - 2018-11-20 101 | 102 | ### Fixed 103 | 104 | - update dependencies 105 | 106 | ## [2.1.0] - 2018-07-22 107 | 108 | ### Added 109 | 110 | - added support of prettier 111 | 112 | ## [2.0.2] - 2018-07-21 113 | 114 | ### Changed 115 | 116 | - documentation updated and reviewed to reflect changes in 2.0.0 117 | 118 | ## [2.0.1] - 2018-07-21 119 | 120 | ### Changed 121 | 122 | - documentation updated to reflect changes in 2.0.0 123 | 124 | ## [2.0.0] - 2018-07-21 125 | 126 | ### Added 127 | 128 | - remove all boilerplate code and create a script (`npm run build-step-mappings`) to automatically bind all steps to feature files. 129 | - this version can be seen as a lightweight cucumber-ts/SpecFlow implementation with full support of all TestCafe and TestCafe Live functionalities. 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testcafe-starter", 3 | "version": "7.4.1", 4 | "description": "starter project for e2e tests with testcafe", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-step-mappings": "ts-node step-mappings/generator", 8 | "build": "npm run prettier-format && npm run lint && npm run tsc", 9 | "eslint-fix": "eslint ./ --ext .js,.ts,.tsx --fix", 10 | "lint": "eslint ./ --ext .js,.ts --format visualstudio --no-color --max-warnings 10 --report-unused-disable-directives --ignore-pattern 'coverage/*' --ignore-pattern 'node_modules/*'", 11 | "prebuild": "rimraf build", 12 | "pretest": "rimraf screenshots && rimraf reports", 13 | "prettier-check": "prettier --list-different \"./**/*.ts\" ", 14 | "prettier-format": "prettier --write \"./**/*.ts\" ", 15 | "report": "ts-node report-generator.ts", 16 | "test:json": "npm test -- --reporter cucumber-json --skip-js-errors", 17 | "test:live": "testcafe chrome features/**/*.live.ts --live --env=local --user=user1@example.com --selector-timeout 10000 --skip-js-errors", 18 | "test:teamcity": "testcafe \"chrome --ignore-certificate-errors\" features/**/*.spec.ts --env=local --user=user1@example.com --selector-timeout 10000 --reporter teamcity -S -s screenshots --quarantine-mode --skip-js-errors", 19 | "test": "testcafe chrome features/**/*.spec.ts --env=local --user=user1@example.com --selector-timeout 10000 -S -s screenshots --skip-js-errors --skip-uncaught-errors --hostname localhost", 20 | "tsc:init": "tsc --init", 21 | "tsc": "tsc" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/hdorgeval/testcafe-starter.git" 26 | }, 27 | "keywords": [ 28 | "testcafe", 29 | "typescript", 30 | "e2e" 31 | ], 32 | "author": "Henri d'Orgeval", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/hdorgeval/testcafe-starter/issues" 36 | }, 37 | "homepage": "https://github.com/hdorgeval/testcafe-starter#readme", 38 | "dependencies": { 39 | "@types/chalk": "2.2.0", 40 | "@types/minimist": "1.2.0", 41 | "@types/node": "14.0.23", 42 | "chalk": "4.1.0", 43 | "jsome": "2.5.0", 44 | "minimist": "1.2.5", 45 | "rimraf": "3.0.2", 46 | "slash": "3.0.0", 47 | "testcafe": "1.8.8", 48 | "testcafe-browser-provider-browserstack": "1.13.0", 49 | "testcafe-reporter-cucumber-json": "6.0.10", 50 | "testcafe-reporter-teamcity": "1.0.12", 51 | "ts-node": "8.10.2", 52 | "typescript": "3.9.6" 53 | }, 54 | "devDependencies": { 55 | "@typescript-eslint/eslint-plugin": "3.6.1", 56 | "@typescript-eslint/parser": "3.6.1", 57 | "eslint": "7.4.0", 58 | "eslint-config-prettier": "6.11.0", 59 | "eslint-plugin-import": "2.22.0", 60 | "eslint-plugin-prettier": "3.1.4", 61 | "eslint-plugin-testcafe": "0.2.1", 62 | "mem": "6.1.0", 63 | "multiple-cucumber-html-reporter": "1.17.0", 64 | "prettier": "2.0.5" 65 | }, 66 | "engines": { 67 | "node": ">=8.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /step-runner.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ButStep, 3 | butStepMappings, 4 | GivenStep, 5 | givenStepMappings, 6 | StepMappings, 7 | ThenStep, 8 | thenStepMappings, 9 | WhenStep, 10 | whenStepMappings, 11 | } from './step-mappings'; 12 | import { symbols } from './tools/string/symbols'; 13 | import { t } from 'testcafe'; 14 | import * as chalk from 'chalk'; 15 | 16 | enum StepLabel { 17 | Given = 'Given', 18 | When = 'When ', 19 | Then = 'Then ', 20 | And = 'And ', 21 | But = 'But ', 22 | } 23 | 24 | function executionOfCurrentTestWasCanceledByPreviousStep(): boolean { 25 | const canExecute: boolean | undefined = t.ctx.canExecute; 26 | return canExecute === false ? true : false; 27 | } 28 | 29 | function showSuccess(stepName: string, stepLabel: StepLabel): void { 30 | if (!t.ctx.stepRunnerContext) { 31 | t.ctx.stepRunnerContext = {}; 32 | 33 | // eslint-disable-next-line no-console 34 | console.log(''); 35 | } 36 | 37 | // eslint-disable-next-line no-console 38 | console.log(` ${chalk.green(symbols.ok)} ${stepLabel} ${stepName}`); 39 | } 40 | 41 | async function executeStep( 42 | stepName: GivenStep | WhenStep | ThenStep | ButStep, 43 | stepMappings: StepMappings, 44 | stepLabel: StepLabel 45 | ): Promise { 46 | if (executionOfCurrentTestWasCanceledByPreviousStep()) { 47 | return; 48 | } 49 | 50 | const foundStep = stepMappings[stepName]; 51 | if (typeof foundStep === 'function') { 52 | await foundStep(stepName as string); 53 | showSuccess(stepName, stepLabel); 54 | return; 55 | } 56 | 57 | throw new Error(`Step "${stepName}" is not mapped to an executable code.`); 58 | } 59 | export async function given(stepName: GivenStep): Promise { 60 | await executeStep(stepName, givenStepMappings, StepLabel.Given); 61 | } 62 | export async function when(stepName: WhenStep): Promise { 63 | await executeStep(stepName, whenStepMappings, StepLabel.When); 64 | } 65 | export async function then(stepName: ThenStep): Promise { 66 | await executeStep(stepName, thenStepMappings, StepLabel.Then); 67 | } 68 | export async function but(stepName: ButStep): Promise { 69 | await executeStep(stepName, butStepMappings, StepLabel.But); 70 | } 71 | export async function and(stepName: GivenStep | WhenStep | ThenStep | ButStep): Promise { 72 | ensureThat(stepName).isNotAmbiguous(); 73 | 74 | if (givenStepMappings[stepName as GivenStep]) { 75 | return executeStep(stepName, givenStepMappings, StepLabel.And); 76 | } 77 | 78 | if (whenStepMappings[stepName as WhenStep]) { 79 | return executeStep(stepName, whenStepMappings, StepLabel.And); 80 | } 81 | 82 | if (thenStepMappings[stepName as ThenStep]) { 83 | return executeStep(stepName, thenStepMappings, StepLabel.And); 84 | } 85 | 86 | if (butStepMappings[stepName as ButStep]) { 87 | return executeStep(stepName, butStepMappings, StepLabel.And); 88 | } 89 | 90 | throw new Error(`Step "${stepName}" is not mapped to an executable code.`); 91 | } 92 | 93 | function ensureThat( 94 | stepName: GivenStep | WhenStep | ThenStep | ButStep 95 | ): { isNotAmbiguous: () => void } { 96 | return { 97 | isNotAmbiguous: (): void => { 98 | if (givenStepMappings[stepName as GivenStep] && thenStepMappings[stepName as ThenStep]) { 99 | throw new Error(`Step "${stepName}" is defined as both a 'Given' and a 'Then' step.`); 100 | } 101 | if (whenStepMappings[stepName as WhenStep] && thenStepMappings[stepName as ThenStep]) { 102 | throw new Error(`Step "${stepName}" is defined as both a 'When' and a 'Then' step.`); 103 | } 104 | if (butStepMappings[stepName as ButStep] && thenStepMappings[stepName as ThenStep]) { 105 | throw new Error(`Step "${stepName}" is defined as both a 'But' and a 'Then' step.`); 106 | } 107 | }, 108 | }; 109 | } 110 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": 5 | "ES2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 6 | "module": 7 | "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation: */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "sourceMap": true /* Generates corresponding '.map' file. */, 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true /* Import emit helpers from 'tslib'. */, 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */, 25 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 26 | "strictNullChecks": true /* Enable strict null checks. */, 27 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 28 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 29 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 30 | "skipLibCheck": true, 31 | 32 | /* Additional Checks */ 33 | "noUnusedLocals": true /* Report errors on unused locals. */, 34 | "noUnusedParameters": true /* Report errors on unused parameters. */, 35 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 36 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 47 | 48 | /* Source Map Options */ 49 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 50 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 51 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 52 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 53 | 54 | /* Experimental Options */ 55 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 56 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /step-mappings/generator/create-steps-mappings.ts: -------------------------------------------------------------------------------- 1 | import { ensureDirectoryStructureExists } from '../../tools/fs/ensure-directory-structure-exists'; 2 | import { getExportedFunctionsIn, FuncInfo } from '../../tools/fs/get-exported-functions-in'; 3 | import { getFileName } from '../../tools/fs/get-filename'; 4 | import { getFilePathWithoutExtension } from '../../tools/fs/get-filepath-without-extension'; 5 | import { getFuncNameFrom } from '../../tools/fs/get-func-name-from-file-name'; 6 | import { getJsDocCommentsOf } from '../../tools/fs/get-jsdoc-of-function'; 7 | import { getRelativePathOf } from '../../tools/fs/get-relative-path-from'; 8 | import { slash } from '../../tools/fs/slash'; 9 | import { surround } from '../../tools/string/surround-with-quote'; 10 | import { upperCaseFirstLetter } from '../../tools/string/upper-case-first-letter'; 11 | import { config } from '../config'; 12 | import { EOL } from 'os'; 13 | import { PathLike, writeFileSync } from 'fs'; 14 | 15 | let exportIndex = -1; 16 | function nextIndex(): number { 17 | exportIndex += 1; 18 | return exportIndex; 19 | } 20 | export const createStepsMapping = (path: PathLike): { from: (stepFiles: string[]) => void } => { 21 | ensureDirectoryStructureExists(path.toString()); 22 | writeFileSync(path, 'importing steps and creating given/when/then mappings...'); 23 | return { 24 | from: (stepFiles: string[]): void => { 25 | const lines: string[] = []; 26 | lines.push( 27 | `// this file was auto-generated by '${getRelativePathOf(__filename).from( 28 | config.stepsMappingFile 29 | )}'` 30 | ); 31 | lines.push(...createImports()); 32 | lines.push(''); 33 | lines.push(...createInterfaces()); 34 | lines.push(...createStepMappingsFrom(stepFiles).forStep('given')); 35 | lines.push(...createStepMappingsFrom(stepFiles).forStep('when')); 36 | lines.push(...createStepMappingsFrom(stepFiles).forStep('then')); 37 | lines.push(...createStepMappingsFrom(stepFiles).forStep('but')); 38 | lines.push(''); 39 | writeFileSync(path, lines.join(EOL)); 40 | }, 41 | }; 42 | }; 43 | 44 | function createInterfaces(): string[] { 45 | const lines = []; 46 | lines.push('export interface StepMappings {'); 47 | lines.push(`${config.tab}[index: string]: (stepname: string) => Promise;`); 48 | lines.push('}'); 49 | return lines; 50 | } 51 | 52 | function createImports(): string[] { 53 | const stepsBarrelRelativePath = getRelativePathOf(config.stepsBarrelFile).from( 54 | config.stepsMappingFile 55 | ); 56 | const lines: string[] = []; 57 | lines.push( 58 | `import * as step from ${config.quoteMark}${slash( 59 | getFilePathWithoutExtension(stepsBarrelRelativePath) 60 | )}${config.quoteMark};` 61 | ); 62 | return lines; 63 | } 64 | 65 | export interface StepMapping { 66 | stepSentence: string; 67 | stepFunc: string; 68 | } 69 | 70 | function createStepMappingsFrom(stepFiles: string[]): { forStep: (step: string) => string[] } { 71 | return { 72 | forStep: (step: string): string[] => { 73 | const lines: string[] = []; 74 | const stepMappings = getStepMappingsFrom(stepFiles) 75 | .forStep(step) 76 | .sort((a: StepMapping, b: StepMapping): number => 77 | a.stepSentence.localeCompare(b.stepSentence) 78 | ); 79 | 80 | if (stepMappings.length === 0) { 81 | lines.push(`export const ${step}StepMappings = {};`); 82 | lines.push( 83 | `export type ${upperCaseFirstLetter(step)}Step = keyof typeof ${step}StepMappings;` 84 | ); 85 | return lines; 86 | } 87 | 88 | lines.push(`export const ${step}StepMappings = {`); 89 | stepMappings.forEach((stepMapping: StepMapping): void => { 90 | lines.push( 91 | `${config.tab}${surround(stepMapping.stepSentence).with(config.quoteMark)}: step.${ 92 | stepMapping.stepFunc 93 | },` 94 | ); 95 | }); 96 | lines.push('};'); 97 | lines.push( 98 | `export type ${upperCaseFirstLetter(step)}Step = keyof typeof ${step}StepMappings;` 99 | ); 100 | return lines; 101 | }, 102 | }; 103 | } 104 | 105 | function getStepMappingsFrom(stepFiles: string[]): { forStep: (step: string) => StepMapping[] } { 106 | return { 107 | forStep: (step: string): StepMapping[] => { 108 | const results: StepMapping[] = []; 109 | stepFiles.forEach((filePath: string): void => { 110 | const fileName = getFileName(filePath) || `defaultStep${nextIndex()}`; 111 | const defaultExportName = getFuncNameFrom(fileName); 112 | getExportedFunctionsIn(filePath).forEach((funcInfo: FuncInfo): void => { 113 | getJsDocCommentsOf(funcInfo) 114 | .filter((comment: string): boolean => comment.includes(`@${step}`)) 115 | .map((comment: string): { stepSentence: string; stepFunc: string } => { 116 | const firstIndex = comment.indexOf('(') + 2; 117 | const lastIndex = comment.lastIndexOf(')') - 1; 118 | const stepSentence = comment.substring(firstIndex, lastIndex); 119 | const stepFunc = 120 | funcInfo.functionName === 'default' ? defaultExportName : funcInfo.functionName; 121 | return { stepSentence, stepFunc }; 122 | }) 123 | .forEach((stepMapping: StepMapping): number => results.push(stepMapping)); 124 | }); 125 | }); 126 | return results; 127 | }, 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TestCafe Starter 2 | 3 | [![Build Status](https://travis-ci.org/hdorgeval/testcafe-starter.svg?branch=master)](https://travis-ci.org/hdorgeval/testcafe-starter) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/7tlvxcnt06yy6umo?svg=true)](https://ci.appveyor.com/project/hdorgeval/testcafe-starter) 5 | 6 | Gherkin with TestCafe 7 | 8 | 9 | ## A lightweight and extensible framework to write e2e tests in a gherkin-like syntax. 10 | 11 | ```typescript 12 | fixture('Feature: TestCafe Example') 13 | .before(async (ctx) => { 14 | // inject global configuration in the fixture context 15 | ctx.config = getCurrentConfig(); 16 | }) 17 | .beforeEach(async (t) => { 18 | // inject page model in the test context 19 | t.ctx.inputData = pageModel; 20 | await given('I navigate to the testcafe sample page'); 21 | }); 22 | 23 | test('Scenario: cannot submit my feedback when I did not enter my name', async () => { 24 | await then('no name should be populated'); 25 | await and('I cannot submit my feedback on testcafe'); 26 | }); 27 | 28 | test('Scenario: can send feedback with my name only', async () => { 29 | await when('I enter my name'); 30 | await then('I can submit my feedback on testcafe'); 31 | }); 32 | 33 | test('Scenario: send feedback', async () => { 34 | await given('I enter my name'); 35 | await when('I send my feedback on testcafe'); 36 | await then("a 'Thank you' message should appear with my name"); 37 | }); 38 | ``` 39 | 40 | ![demo](./.media/demo1.gif) 41 | 42 | ## Benefit from TypeScript Strong Typing and Visual Studio Code IntelliSense to write tests that are aligned with the business 43 | 44 | ![demo](./.media/demo3.gif) 45 | 46 | ## After cloning the repo 47 | 48 | - run the command `npm install`. 49 | 50 | ## To execute the tests locally 51 | 52 | - run the command `npm test`. 53 | 54 | ## To execute the tests locally with an HTML report of tests execution 55 | 56 | - run the commands: 57 | 58 | ```sh 59 | npm run test:json 60 | npm run report 61 | ``` 62 | 63 | This will generate a nice and searchable HTML report like this ([more details here](https://github.com/hdorgeval/testcafe-reporter-cucumber-json)): 64 | 65 | ![report](./.media/report01.png) 66 | 67 | ## To execute the tests on TeamCity 68 | 69 | - run the command `npm run test:teamcity`. 70 | 71 | ## To configure the target environment and the target persona 72 | 73 | - add the following options to the TestCafe command-line `--env=xxx --user=yyy` 74 | - you can create any type of option on the command-line: see the [readme](config/README.md) in the [config](config) folder. 75 | 76 | ## To create custom command-line options on top of TestCafe command-line options 77 | 78 | - You can add any custom command-line options to the existing TestCafe command-line options. 79 | - To do this, customize the content of [parsed-config.ts](config/parsed-config.ts). 80 | 81 | ## To check for typescript and linting errors 82 | 83 | - run the command `npm run build`. 84 | 85 | ## To debug a test in Visual Studio Code 86 | 87 | - set one or more breakpoints in your code 88 | - setup the TestCafe configuration used by the debug session in the [default-config.ts](config/default-config.ts) file 89 | - in the Debug menu, select the `TestCafe` option 90 | - if you want to start TestCafe with specific command-line options you can modify the [launch.json](.vscode/launch.json) file 91 | - Start debugging 92 | 93 | ## To use Live mode 94 | 95 | Live mode provides a service that keeps the TestCafe process and browsers opened the whole time you are working on tests. Changes you make in code immediately restart the tests. That is, TestCafe Live allows you to see test results instantly. 96 | 97 | - rename the feature file into a name that ends with .live.ts 98 | - add .only to the test(s) on which you want to work live 99 | - run the command `npm run test:live` 100 | 101 | ## Visual Studio Code requirements 102 | 103 | - the VS Code version must be >= 1.18.0 104 | 105 | ## Recommended Visual Studio Code Extensions 106 | 107 | - Git Lens 108 | - Regex Previewer by Christof Marti 109 | - TestCafe Snippets (see [say goodbye to flakyness](https://github.com/hdorgeval/testcafe-snippets)) 110 | - TestCafe Test Runner (see [How to execute a test from Visual Studio Code IDE](#how-to-execute-a-test-from-visual-studio-code-ide)) 111 | 112 | ## How to jump into the implementation of a step (Visual Studio Code) 113 | 114 | - Go to the Command Palette with ⌘P (Ctrl+P on Windows) 115 | 116 | - Start typing the sentence. For example `I send my feedback on testcafe` 117 | 118 | - select the found step file: 119 | 120 | ![find the step implementation](./.media/screenshot08.png) 121 | 122 | ## How to create a new feature file 123 | 124 | - see the [readme](features/README.md) 125 | 126 | ## How to create a new step-definition file 127 | 128 | - see the [readme](steps/README.md) 129 | 130 | ## How to run a test only in specific environment(s) 131 | 132 | - The environment is the host that will execute the TestCafe tests. 133 | - The environment is set in the config object injected at runtime in the Fixture Context. 134 | - All possible values are, by convention, defined in the [environments.ts](config/environments.ts) file 135 | 136 | - Add this line as a first step in the test: 137 | 138 | ```typescript 139 | test('Scenario: send feedback', async () => { 140 | await env.only('devci'); 141 | // 142 | await given('I enter my name'); 143 | await when('I send my feedback on testcafe'); 144 | await then("a 'Thank you' message should appear with my name"); 145 | }); 146 | ``` 147 | 148 | ![demo](./.media/demo2.gif) 149 | 150 | - to select another environment, just use the VS Code IntelliSense: 151 | ![available environments](./.media/screenshot04.png) 152 | 153 | - to target multiple environments: 154 | 155 | ```typescript 156 | test('Scenario: send feedback', async () => { 157 | await env.only('uat', 'devci'); 158 | // 159 | await given('I enter my name'); 160 | await when('I send my feedback on testcafe'); 161 | await then("a 'Thank you' message should appear with my name"); 162 | }); 163 | ``` 164 | 165 | ## How to execute a test from Visual Studio Code IDE 166 | 167 | To start a test from the IDE you need to install the Visual Studio Code extension [TestCafe Test Runner](https://github.com/romanresh/vscode-testcafe). 168 | 169 | ### To run a specific test 170 | 171 | Right-click on the test and and select TestCafe: `Run Test(s) in...` for the required browser. 172 | 173 | ### To run all tests in a feature file 174 | 175 | Right-click on the feature file within the Explorer panel and select TestCafe: `Run Test(s) in...` for the required browser. 176 | --------------------------------------------------------------------------------