├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── runtime
├── scripts
│ ├── precommit-checks.sh
│ ├── projectList.csv
│ └── tagList.json
├── img
│ ├── klassiLogo.png
│ ├── javascript-code.jpg
│ ├── installapp-support.jpg
│ ├── cucumber-html-report.png
│ └── accessibility-html-report.png
├── configLoader.js
├── getRemote.js
├── getVideoLinks.js
├── coding-standards
│ ├── gherkin
│ │ └── gherkin-lint.json
│ └── eslint
│ │ └── eslint.config.js
├── utils
│ ├── retrieveMailinator.js
│ ├── takeReportBackup.js
│ └── klassiOutlook.js
├── drivers
│ ├── firefoxDriver.js
│ ├── chromeDriver.js
│ └── lambdatestDriver.js
├── remotes
│ └── lambdatest.js
├── reporter
│ └── reporter.js
├── s3Upload.js
├── s3ReportProcessor.js
├── world.js
├── docs
│ └── klassi-js-v6-Upgrade.md
├── mailer.js
└── helpers.js
├── commitlint.config.js
├── .gitignore
├── .env.example
├── __tests__
├── unit
│ └── runtime
│ │ ├── configLoader.test.js
│ │ ├── firefoxDriver.js
│ │ ├── getVideoLinks.js
│ │ ├── getRemote.js
│ │ ├── chromeDriver.js
│ │ └── s3Upload.js
└── integration
│ └── runtime
│ ├── configLoader.test.js
│ ├── firefoxDriver.js
│ ├── getVideoLinks.js
│ ├── getRemote.js
│ ├── chromeDriver.js
│ └── s3Upload.js
├── .dataConfigrc.js
├── .envConfigrc.js
├── branchnamelinter.config.json
├── jest.config.js
├── .versionrc.json
├── klassiModule.js
├── cucumber.js
├── LICENSE
├── CHANGELOG.md
├── package.json
├── .circleci
└── config.yml
├── index.js
└── README.md
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
--------------------------------------------------------------------------------
/runtime/scripts/precommit-checks.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | pnpm lint
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | npx --no -- commitlint --edit "${1}"
3 | pnpm lint-branch-name
--------------------------------------------------------------------------------
/runtime/img/klassiLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klassijs/klassi-js/HEAD/runtime/img/klassiLogo.png
--------------------------------------------------------------------------------
/runtime/img/javascript-code.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klassijs/klassi-js/HEAD/runtime/img/javascript-code.jpg
--------------------------------------------------------------------------------
/runtime/img/installapp-support.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klassijs/klassi-js/HEAD/runtime/img/installapp-support.jpg
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh".
3 |
4 | ./runtime/scripts/precommit-checks.sh
5 |
--------------------------------------------------------------------------------
/runtime/img/cucumber-html-report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klassijs/klassi-js/HEAD/runtime/img/cucumber-html-report.png
--------------------------------------------------------------------------------
/runtime/img/accessibility-html-report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klassijs/klassi-js/HEAD/runtime/img/accessibility-html-report.png
--------------------------------------------------------------------------------
/runtime/scripts/projectList.csv:
--------------------------------------------------------------------------------
1 | Projects,Branch,Folder
2 | jlops-test-suite, main
3 | elt_lti_management-js,master
4 | elt_tao-js,main
5 |
6 |
--------------------------------------------------------------------------------
/runtime/scripts/tagList.json:
--------------------------------------------------------------------------------
1 | {
2 | "tagNames": [
3 | "@api",
4 | "@tapi",
5 | "@get",
6 | "@put",
7 | "@post",
8 | "@delete",
9 | "@s3load",
10 | "@plutora",
11 | "@zephyr",
12 | "@s3"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/runtime/configLoader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const fs = require('fs-extra');
6 |
7 | module.exports = (configFilePath) => {
8 | return JSON.parse(fs.readFileSync(configFilePath, 'utf8'));
9 | // TODO: add validation if schema
10 | };
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## project folders
2 | artifacts
3 |
4 | # exclude generated reports
5 | reports
6 | reportBackup
7 | coverage
8 |
9 | # ignore dotenv files, except example
10 | .env
11 | .env.*
12 | !.env.example
13 | env.*
14 |
15 | # Logs
16 | logs
17 | log
18 | *.log
19 | npm-debug.log*
20 |
21 | # Dependency directories
22 | node_modules
23 |
24 | # IntelliJ project files
25 | .idea
26 | out
27 | gen
28 | .DS_Store
29 | *.iml
30 |
31 | #tesseract
32 | eng.traineddata
33 | *.tgz
34 |
35 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # AWS credentials for S3 bucket access
2 | S3_KEY=AWS-ACCESS-KEY-FOR-S3-BUCKET-ACCESS
3 | S3_SECRET=AWS-SECRET-KEY-FOR-S3-BUCKET-ACCESS
4 |
5 | # AWS credentials for SES email sending
6 | SES_KEY=AWS-ACCESS-KEY-FOR-SES-ACCESS
7 | SES_SECRET=AWS-SECRET-KEY-FOR-SES-ACCESS
8 |
9 | LAMBDATEST_USERNAME=YOUR-LAMBDATEST-USERNAME
10 | LAMBDATEST_API_URL=YOUR-LAMBDATEST-API-URL
11 | LAMBDATEST_ACCESS_KEY=YOUR-LAMBDATEST-ACCESS-KEY
12 |
13 | #Zephyr Credentials
14 | ACCESS_TOKEN=YOUR_ACCESS_TOKEN
15 |
16 |
--------------------------------------------------------------------------------
/__tests__/unit/runtime/configLoader.test.js:
--------------------------------------------------------------------------------
1 | const configLoader = require('../../../runtime/configLoader');
2 | const fs = require('fs-extra');
3 |
4 | jest.mock('fs-extra');
5 |
6 | describe('configLoader', () => {
7 | it('should return a parsed JSON.', () => {
8 | const input = '{"test": "example"}';
9 | fs.readFileSync.mockImplementationOnce(() => input);
10 |
11 | const actual = configLoader(input);
12 | const expected = {
13 | test: 'example'
14 | };
15 | expect(actual).toEqual(expected);
16 | });
17 | });
--------------------------------------------------------------------------------
/__tests__/integration/runtime/configLoader.test.js:
--------------------------------------------------------------------------------
1 | const configLoader = require('../../../runtime/configLoader');
2 | const fs = require('fs-extra');
3 |
4 | jest.mock('fs-extra');
5 |
6 | describe('configLoader', () => {
7 | it('should return a parsed JSON.', () => {
8 | const input = '{"test": "example"}';
9 | fs.readFileSync.mockImplementationOnce(() => input);
10 |
11 | const actual = configLoader(input);
12 | const expected = {
13 | test: 'example'
14 | };
15 | expect(actual).toEqual(expected);
16 | });
17 | });
--------------------------------------------------------------------------------
/.dataConfigrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | dataConfig: {
3 | projectName: 'klassi-js',
4 | s3FolderName: 'klassi-js',
5 |
6 | emailData: {
7 | nameList: 'QaAutoTest ',
8 | AccessibilityReport: 'Yes',
9 | SES_REGION: 'eu-west-1',
10 | },
11 |
12 | s3Data: {
13 | S3_BUCKET: 'test-app-automated-reports',
14 | S3_REGION: 'eu-west-2',
15 | S3_DOMAIN_NAME: 'http://test-app-automated-reports.s3.eu-west-2.amazonaws.com',
16 | },
17 |
18 | tagNames: [],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/.envConfigrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /**
3 | * this is for your project environment setups
4 | * all relevant details for each environment i.e. urls, username, passwords etc.
5 | */
6 | "environment": {
7 | "dev": {
8 | "envName": "DEVELOPMENT",
9 | "base_url": "https://duckduckgo.com/",
10 | "api_base_url": "http://httpbin.org/get",
11 | },
12 |
13 | "test": {
14 | "envName": "TEST",
15 | "base_url": "https://duckduckgo.com/",
16 | "api_base_url": "http://httpbin.org/get",
17 | },
18 |
19 | "uat": {
20 | "envName": "UAT",
21 | "base_url": ""
22 | }
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/runtime/getRemote.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const lambdatest = require('./remotes/lambdatest');
6 |
7 | module.exports = function getRemote(remoteService) {
8 | const remote = {};
9 |
10 | function noop() {
11 | console.log('"If you\'re seeing this, you\'re trying to run a non-existent remoteService"');
12 | }
13 | if (!remoteService) {
14 | remote.type = 'disabled';
15 | remote.after = noop;
16 | } else if (remoteService === 'lambdatest') {
17 | remote.type = 'lambdatest';
18 | remote.after = lambdatest.submitResults;
19 | } else {
20 | console.log(`Unknown remote service ${remoteService}`);
21 | remote.type = 'unknown';
22 | remote.after = noop;
23 | }
24 | return remote;
25 | };
26 |
--------------------------------------------------------------------------------
/branchnamelinter.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "branchNameLinter": {
3 | "prefixes": [
4 | "feature",
5 | "testfix",
6 | "automation",
7 | "hotfix",
8 | "release"
9 | ],
10 | "suggestions": {
11 | "feat": "feature",
12 | "fix": "testfix",
13 | "auto": "automation"
14 | },
15 | "disallowed": ["master", "main", "develop", "staging"],
16 | "separator": "/",
17 | "msgBranchBanned": "Branches with the name \"%s\" are not allowed.",
18 | "msgBranchDisallowed": "Pushing to \"%s\" is not allowed, use git-flow.",
19 | "msgPrefixNotAllowed": "Branch prefix \"%s\" is not allowed.",
20 | "msgPrefixSuggestion": "Instead of \"%s\" try \"%s\".",
21 | "msgseparatorRequired": "Branch \"%s\" must contain a separator \"%s\"."
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/runtime/getVideoLinks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const pactumJs = require('pactum');
6 |
7 | /**
8 | * setting the envConfig variables for file list
9 | */
10 | const ltUrl = process.env.LAMBDATEST_API_URL;
11 | const ltUsername = process.env.LAMBDATEST_USERNAME;
12 | const ltKey = process.env.LAMBDATEST_ACCESS_KEY;
13 |
14 | let res;
15 | let videoID;
16 | let url;
17 |
18 | module.exports = {
19 | getVideoList: async () => {
20 | const { sessionId } = browser;
21 | url = `https://${ltUrl}/sessions/${sessionId}/video`;
22 | res = await pactumJs.spec().get(url).withAuth(ltUsername, ltKey).expectStatus(200).toss();
23 | videoID = res.body.url;
24 | return videoID;
25 | },
26 |
27 | getVideoId: async () => {
28 | console.log('this is the failed test video link ', videoID);
29 | return videoID;
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | /** @type {import('jest').Config} */
7 | const config = {
8 | clearMocks: true,
9 | collectCoverage: true,
10 | coverageDirectory: 'reports',
11 | coverageProvider: 'v8',
12 | coverageReporters: ['json'],
13 | reporters: ['default', [ 'jest-junit', { suiteName: 'jest tests' } ]],
14 | maxWorkers: 2,
15 | testMatch: [
16 | '**/__tests__/**/*.[jt]s?(x)',
17 | '**/?(*.)+(spec|test).[tj]s?(x)'
18 | ],
19 | testPathIgnorePatterns: [
20 | '/node_modules/'
21 | ],
22 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
23 | // watchPathIgnorePatterns: [],
24 |
25 | // Whether to use watchman for file crawling
26 | // watchman: true,
27 | };
28 |
29 | module.exports = config;
30 |
--------------------------------------------------------------------------------
/.versionrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "types": [
3 | {
4 | "type": "feat",
5 | "section": "Features"
6 | },
7 | {
8 | "type": "fix",
9 | "section": "Bug Fixes"
10 | },
11 | {
12 | "type": "docs",
13 | "section": "Documentation"
14 | },
15 | {
16 | "type": "refactor",
17 | "section": "Refactors"
18 | },
19 | {
20 | "type": "chore",
21 | "hidden": true
22 | },
23 | {
24 | "type": "docs",
25 | "hidden": false
26 | },
27 | {
28 | "type": "style",
29 | "hidden": true
30 | },
31 | {
32 | "type": "refactor",
33 | "hidden": false
34 | },
35 | {
36 | "type": "perf",
37 | "hidden": true
38 | },
39 | {
40 | "type": "test",
41 | "hidden": true
42 | }
43 | ],
44 | "commitUrlFormat": "https://github.com/klassijs/klassi-js/commit/{{hash}}",
45 | "compareUrlFormat": "https://github.com/klassijs/klassi-js/compare/{{previousTag}}...{{currentTag}}"
46 | }
47 |
--------------------------------------------------------------------------------
/klassiModule.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This module is used to export all the required modules from Klassi-js to be consumed by all projects
3 | */
4 |
5 | const safeRequire = (moduleName) => {
6 | try {
7 | return require(moduleName);
8 | } catch (error) {
9 | console.warn(`Warning: Could not load module '${moduleName}': ${error.message}`);
10 | return null;
11 | }
12 | };
13 |
14 | module.exports = {
15 | pactum: safeRequire('pactum'),
16 | webdriverio: safeRequire('webdriverio'),
17 | fs: safeRequire('fs-extra'),
18 | dotenv: safeRequire('dotenv'),
19 | S3Client: (() => {
20 | try {
21 | return require('@aws-sdk/client-s3').S3Client;
22 | } catch (error) {
23 | console.warn(`Warning: Could not load S3Client: ${error.message}`);
24 | return null;
25 | }
26 | })(),
27 | softAssert: safeRequire('klassijs-soft-assert'),
28 | visualValidation: safeRequire('klassijs-visual-validation'),
29 | a11yValidator: safeRequire('klassijs-a11y-validator'),
30 | astellen: safeRequire('klassijs-astellen'),
31 | smartOcr: safeRequire('klassijs-smart-ocr'),
32 | };
33 |
--------------------------------------------------------------------------------
/runtime/coding-standards/gherkin/gherkin-lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "no-empty-file": "on",
3 | "file-name": ["on", {"style": "camelCase"}],
4 | "indentation" : [
5 | "on", {
6 | "Feature": 0,
7 | "Background": 2,
8 | "Scenario": 2,
9 | "Step": 4,
10 | "Examples": 4,
11 | "example": 6,
12 | "given": 4,
13 | "when": 4,
14 | "then": 4,
15 | "and": 4,
16 | "but": 4,
17 | "feature tag": 0,
18 | "scenario tag": 2
19 | }
20 | ],
21 | "no-multiple-empty-lines": "on",
22 | "no-superfluous-tags": "on",
23 | "no-unused-variables": "on",
24 | "keywords-in-logical-order": "on",
25 | "use-and": "on",
26 | "no-dupe-scenario-names": "on",
27 | "name-length" : ["on", { "Feature": 70, "Scenario": 70, "Step": 70 }],
28 | "scenario-size": ["on", { "steps-length": { "Background": 5, "Scenario": 10, "Examples": 10 } }],
29 | "max-scenarios-per-file": ["on", { "maxScenarios": 10, "countOutlineExamples": false }],
30 | "no-files-without-scenarios": "on",
31 | "no-examples-in-scenarios": "on",
32 | "no-scenario-outlines-without-examples": "on"
33 | }
34 |
--------------------------------------------------------------------------------
/cucumber.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const path = require('path');
6 | const envName = env.envName.toLowerCase();
7 |
8 | const options = {
9 | default: {
10 | dryRun: dryRun,
11 | // eslint-disable-next-line no-undef
12 | paths: featureFiles,
13 | require: ['runtime/world.js', 'node_modules/klassi-js/runtime/world.js', 'step_definitions/**/*.js'],
14 | tags: global.resultingString,
15 | format: [
16 | '@cucumber/pretty-formatter',
17 | `json:${path.resolve(__dirname, paths.reports, browserName, envName, `${reportName}-${dateTime}.json`)}`,
18 | ],
19 | formatOptions: {
20 | colorsEnabled: true,
21 | },
22 | },
23 |
24 | /**
25 | * This is to allow tests tagged as APIs to run headless and NOT take a screenshot on error
26 | * @returns {Promise<*[]>}
27 | */
28 | filterQuietTags: async () => {
29 | let filePath = path.resolve(global.projectRootPath, './runtime/scripts/tagList.json');
30 | let filelist = await helpers.readFromJson(filePath);
31 | return [...filelist.tagNames, ...tagNames];
32 | },
33 | };
34 |
35 | module.exports = options;
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 klassi-js Larry Goddard
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 |
--------------------------------------------------------------------------------
/__tests__/unit/runtime/firefoxDriver.js:
--------------------------------------------------------------------------------
1 | jest.mock('webdriverio', () => ({
2 | remote: jest.fn(),
3 | }));
4 |
5 | jest.mock('@cucumber/cucumber', () => ({
6 | Before: jest.fn(),
7 | }));
8 |
9 | const { filterQuietTags } = require('../../../cucumber.js');
10 |
11 | jest.mock('../../../cucumber.js', () => ({
12 | filterQuietTags: jest.fn(),
13 | }));
14 |
15 | const webdriverio = require('webdriverio');
16 | const firefoxDriver = require('../../../runtime/drivers/firefoxDriver');
17 |
18 | describe('firefoxDriver', () => {
19 | let mockBrowser;
20 |
21 | beforeEach(() => {
22 | mockBrowser = { setWindowSize: jest.fn() };
23 | webdriverio.remote.mockResolvedValue(mockBrowser);
24 | });
25 |
26 | it('should run in headless mode when isApiTest is true', async () => {
27 | filterQuietTags.mockResolvedValue([]);
28 | await firefoxDriver({ headless: true });
29 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
30 | });
31 |
32 | it('should NOT run in headless mode when isApiTest is false', async () => {
33 | filterQuietTags.mockResolvedValue([]);
34 | await firefoxDriver({headless: false});
35 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/__tests__/integration/runtime/firefoxDriver.js:
--------------------------------------------------------------------------------
1 | jest.mock('webdriverio', () => ({
2 | remote: jest.fn(),
3 | }));
4 |
5 | jest.mock('@cucumber/cucumber', () => ({
6 | Before: jest.fn(),
7 | }));
8 |
9 | const { filterQuietTags } = require('../../../cucumber.js');
10 |
11 | jest.mock('../../../cucumber.js', () => ({
12 | filterQuietTags: jest.fn(),
13 | }));
14 |
15 | const webdriverio = require('webdriverio');
16 | const firefoxDriver = require('../../../runtime/drivers/firefoxDriver');
17 |
18 | describe('firefoxDriver', () => {
19 | let mockBrowser;
20 |
21 | beforeEach(() => {
22 | mockBrowser = { setWindowSize: jest.fn() };
23 | webdriverio.remote.mockResolvedValue(mockBrowser);
24 | });
25 |
26 | it('should run in headless mode when isApiTest is true', async () => {
27 | filterQuietTags.mockResolvedValue([]);
28 | await firefoxDriver({ headless: true });
29 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
30 | });
31 |
32 | it('should NOT run in headless mode when isApiTest is false', async () => {
33 | filterQuietTags.mockResolvedValue([]);
34 | await firefoxDriver({headless: false});
35 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [6.1.0](https://github.com/klassijs/klassi-js/compare/v6.0.1...v6.1.0) (2025-11-04)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * **browser:** fixed the browsedr not being initialised ([4bdb524](https://github.com/klassijs/klassi-js/commit/4bdb5246df38514b3306f5c9875057a0250e82e5))
11 | * **browser:** fixed the browsedr not being initialised ([#137](https://github.com/klassijs/klassi-js/issues/137)) ([134fbc8](https://github.com/klassijs/klassi-js/commit/134fbc86420f3223a60602bb6110d3b265867f83))
12 |
13 | ### [6.0.2](https://github.com/klassijs/klassi-js/compare/v6.0.1...v6.0.2) (2025-10-14)
14 |
15 | ## 6.0.0 (2025-07-16)
16 |
17 |
18 | ### Features
19 |
20 | * **klassiModules, klassiOutlook:** added new features, updating config code ([54ad1e0](https://github.com/klassijs/klassi-js/commit/54ad1e086a7336d373274c57ab3689a05403dd01))
21 | * **klassiModules, klassiOutlook:** added new features, updating config code ([5f487d4](https://github.com/klassijs/klassi-js/commit/5f487d440a1a78bd6129bc7e78e2d2810d9a5e08))
22 | * **klassiModules:** updating code ([e8ad843](https://github.com/klassijs/klassi-js/commit/e8ad84325532ed6f8748c3e99458a1fa3f6dbbdd))
23 |
--------------------------------------------------------------------------------
/runtime/utils/retrieveMailinator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const options = {
6 | headers: {
7 | Authorization: sharedObjects.salesforceData.credentials.mailinator.apiKey,
8 | },
9 | throwHttpErrors: true,
10 | simple: false,
11 | allowGetBody: true,
12 | resolveWithFullResponse: true,
13 | };
14 |
15 | const retrieveAllEmails = async (inbox) => {
16 | try {
17 | options.url = `${env.mailinatorApiBaseUrl}/${inbox}`;
18 | const response = await helpers.apiCall(options.url, 'GET', options.headers.Authorization, null);
19 |
20 | return response.body.msgs;
21 | } catch (e) {
22 | console.error(e);
23 | }
24 | };
25 |
26 | const getVerificationCode = async (inbox) => {
27 | let messageId;
28 | let allCurrentReceivedEmails = [];
29 |
30 | while (allCurrentReceivedEmails.length < 1) {
31 | try {
32 | allCurrentReceivedEmails = await retrieveAllEmails(inbox);
33 | } catch (e) {
34 | console.error(e);
35 | }
36 | }
37 |
38 | messageId = allCurrentReceivedEmails[allCurrentReceivedEmails.length - 1].id;
39 | options.url = `${env.mailinatorApiBaseUrl}/${inbox}/messages/${messageId}`;
40 |
41 | const verificationEmail = await helpers.apiCall(options.url, 'GET', options.headers.Authorization, null);
42 | const verificationEmailBody = verificationEmail.parts[0].body;
43 | const verificationCode = verificationEmailBody.match(/Código de verificación: (\d+)/g)[0].match(/\d+/g)[0];
44 | await helpers.apiCall(options.url, 'DELETE', options.headers.Authorization, null);
45 | return verificationCode;
46 | };
47 |
48 | module.exports = getVerificationCode;
49 |
--------------------------------------------------------------------------------
/__tests__/unit/runtime/getVideoLinks.js:
--------------------------------------------------------------------------------
1 | process.env.LAMBDATEST_API_URL = 'mockApiUrl';
2 | process.env.LAMBDATEST_USERNAME = 'mockUsername';
3 | process.env.LAMBDATEST_ACCESS_KEY = 'mockAccessKey';
4 |
5 | const pactumJs = require('pactum');
6 | const { getVideoList, getVideoId } = require('../../../runtime/getVideoLinks');
7 |
8 | jest.mock('pactum', () => ({
9 | spec: jest.fn().mockReturnThis(),
10 | get: jest.fn().mockReturnThis(),
11 | withAuth: jest.fn().mockReturnThis(),
12 | expectStatus: jest.fn().mockReturnThis(),
13 | toss: jest.fn().mockResolvedValue({ body: { url: 'mockVideoUrl' } }),
14 | }));
15 |
16 | describe('getVideoLinks', () => {
17 | beforeEach(() => {
18 | global.browser = { sessionId: 'mockSessionId' };
19 | });
20 |
21 | afterEach(() => {
22 | delete global.browser;
23 | delete process.env.LAMBDATEST_API_URL;
24 | delete process.env.LAMBDATEST_USERNAME;
25 | delete process.env.LAMBDATEST_ACCESS_KEY;
26 | });
27 |
28 | it('should get video list and return video ID', async () => {
29 | const videoID = await getVideoList();
30 |
31 | expect(pactumJs.spec).toHaveBeenCalled();
32 | expect(pactumJs.get).toHaveBeenCalledWith(
33 | `https://${process.env.LAMBDATEST_API_URL}/sessions/${global.browser.sessionId}/video`
34 | );
35 | expect(pactumJs.withAuth).toHaveBeenCalledWith(
36 | `${process.env.LAMBDATEST_USERNAME}`, `${process.env.LAMBDATEST_ACCESS_KEY}`
37 | );
38 | expect(pactumJs.expectStatus).toHaveBeenCalledWith(200);
39 | expect(videoID).toBe('mockVideoUrl');
40 | });
41 |
42 | it('should return the video ID', async () => {
43 | const videoID = await getVideoId();
44 | expect(videoID).toBe('mockVideoUrl');
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/__tests__/integration/runtime/getVideoLinks.js:
--------------------------------------------------------------------------------
1 | process.env.LAMBDATEST_API_URL = 'mockApiUrl';
2 | process.env.LAMBDATEST_USERNAME = 'mockUsername';
3 | process.env.LAMBDATEST_ACCESS_KEY = 'mockAccessKey';
4 |
5 | const pactumJs = require('pactum');
6 | const { getVideoList, getVideoId } = require('../../../runtime/getVideoLinks');
7 |
8 | jest.mock('pactum', () => ({
9 | spec: jest.fn().mockReturnThis(),
10 | get: jest.fn().mockReturnThis(),
11 | withAuth: jest.fn().mockReturnThis(),
12 | expectStatus: jest.fn().mockReturnThis(),
13 | toss: jest.fn().mockResolvedValue({ body: { url: 'mockVideoUrl' } }),
14 | }));
15 |
16 | describe('getVideoLinks', () => {
17 | beforeEach(() => {
18 | global.browser = { sessionId: 'mockSessionId' };
19 | });
20 |
21 | afterEach(() => {
22 | delete global.browser;
23 | delete process.env.LAMBDATEST_API_URL;
24 | delete process.env.LAMBDATEST_USERNAME;
25 | delete process.env.LAMBDATEST_ACCESS_KEY;
26 | });
27 |
28 | it('should get video list and return video ID', async () => {
29 | const videoID = await getVideoList();
30 |
31 | expect(pactumJs.spec).toHaveBeenCalled();
32 | expect(pactumJs.get).toHaveBeenCalledWith(
33 | `https://${process.env.LAMBDATEST_API_URL}/sessions/${global.browser.sessionId}/video`
34 | );
35 | expect(pactumJs.withAuth).toHaveBeenCalledWith(
36 | `${process.env.LAMBDATEST_USERNAME}`, `${process.env.LAMBDATEST_ACCESS_KEY}`
37 | );
38 | expect(pactumJs.expectStatus).toHaveBeenCalledWith(200);
39 | expect(videoID).toBe('mockVideoUrl');
40 | });
41 |
42 | it('should return the video ID', async () => {
43 | const videoID = await getVideoId();
44 | expect(videoID).toBe('mockVideoUrl');
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/__tests__/unit/runtime/getRemote.js:
--------------------------------------------------------------------------------
1 | const getRemote = require('../../../runtime/getRemote');
2 | const lambdatest = require('../../../runtime/remotes/lambdatest');
3 |
4 | jest.mock('../../../runtime/remotes/lambdatest', () => ({
5 | submitResults: jest.fn(),
6 | }));
7 |
8 | describe('getRemote', () => {
9 | it('should return disabled remote when no remoteService is provided', () => {
10 | const remote = getRemote();
11 |
12 | expect(remote.type).toBe('disabled');
13 | expect(remote.after).toBeInstanceOf(Function);
14 | expect(remote.after()).toBeUndefined();
15 | });
16 |
17 | it('should return lambdatest remote when remoteService is lambdatest', () => {
18 | const remote = getRemote('lambdatest');
19 |
20 | expect(remote.type).toBe('lambdatest');
21 | expect(remote.after).toBe(lambdatest.submitResults);
22 | });
23 |
24 | it('should return unknown remote when remoteService is unknown', () => {
25 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
26 | const remote = getRemote('unknownService');
27 |
28 | expect(consoleSpy).toHaveBeenCalledWith('Unknown remote service unknownService');
29 | expect(remote.type).toBe('unknown');
30 | expect(remote.after).toBeInstanceOf(Function);
31 | expect(remote.after()).toBeUndefined();
32 |
33 | consoleSpy.mockRestore();
34 | });
35 |
36 | it('should log a message when noop function is called', () => {
37 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
38 | const remote = getRemote('unknownService');
39 |
40 | remote.after();
41 |
42 | expect(consoleSpy).toHaveBeenCalledWith('"If you\'re seeing this, you\'re trying to run a non-existent remoteService"');
43 |
44 | consoleSpy.mockRestore();
45 | });
46 | });
--------------------------------------------------------------------------------
/__tests__/integration/runtime/getRemote.js:
--------------------------------------------------------------------------------
1 | const getRemote = require('../../../runtime/getRemote');
2 | const lambdatest = require('../../../runtime/remotes/lambdatest');
3 |
4 | jest.mock('../../../runtime/remotes/lambdatest', () => ({
5 | submitResults: jest.fn(),
6 | }));
7 |
8 | describe('getRemote', () => {
9 | it('should return disabled remote when no remoteService is provided', () => {
10 | const remote = getRemote();
11 |
12 | expect(remote.type).toBe('disabled');
13 | expect(remote.after).toBeInstanceOf(Function);
14 | expect(remote.after()).toBeUndefined();
15 | });
16 |
17 | it('should return lambdatest remote when remoteService is lambdatest', () => {
18 | const remote = getRemote('lambdatest');
19 |
20 | expect(remote.type).toBe('lambdatest');
21 | expect(remote.after).toBe(lambdatest.submitResults);
22 | });
23 |
24 | it('should return unknown remote when remoteService is unknown', () => {
25 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
26 | const remote = getRemote('unknownService');
27 |
28 | expect(consoleSpy).toHaveBeenCalledWith('Unknown remote service unknownService');
29 | expect(remote.type).toBe('unknown');
30 | expect(remote.after).toBeInstanceOf(Function);
31 | expect(remote.after()).toBeUndefined();
32 |
33 | consoleSpy.mockRestore();
34 | });
35 |
36 | it('should log a message when noop function is called', () => {
37 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
38 | const remote = getRemote('unknownService');
39 |
40 | remote.after();
41 |
42 | expect(consoleSpy).toHaveBeenCalledWith('"If you\'re seeing this, you\'re trying to run a non-existent remoteService"');
43 |
44 | consoleSpy.mockRestore();
45 | });
46 | });
--------------------------------------------------------------------------------
/runtime/utils/takeReportBackup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This module will clear the local report forlder based on different folder
3 | * Implemented by Sudipta Bhunia
4 | */
5 | const fs = require('fs-extra');
6 | const path = require('path');
7 |
8 | module.exports = {
9 | /**
10 | * This function will take the back-up of the report folder
11 | * @param {string} folderName - The name of the folder to be backed up
12 | */
13 | backupReport: () => {
14 | const rootBackupFolder = path.resolve(settings.projectRoot, './reportBackup');
15 | let dateTime = new Date();
16 | try {
17 | let newFolderName = `${dateTime
18 | .toISOString()
19 | .slice(0, 10)} ${dateTime.getHours()}-${dateTime.getMinutes()}-${dateTime.getSeconds()}`;
20 | const reportBackupFolder = `${rootBackupFolder}/${newFolderName}`;
21 | if (!fs.existsSync(rootBackupFolder)) {
22 | fs.mkdirSync(rootBackupFolder);
23 | }
24 |
25 | if (!fs.existsSync(reportBackupFolder)) {
26 | fs.mkdirSync(reportBackupFolder);
27 | }
28 | fs.copySync('reports', reportBackupFolder);
29 | fs.rmSync('reports', { recursive: true });
30 | console.info(`report back-up taken in ${reportBackupFolder}`);
31 | } catch (err) {
32 | console.error('Error during report back-up process / nothing availabe for back-up');
33 | }
34 | },
35 |
36 | /**
37 | * This function will clear the local report folder
38 | * @param {string} folderName - The name of the folder to be cleared
39 | */
40 | clearReport: () => {
41 | const reportFolder = path.resolve(settings.projectRoot, './reports');
42 | try {
43 | fs.rmSync(reportFolder, { recursive: true });
44 | } catch (err) {
45 | console.error('Unable to clear local reports folder');
46 | }
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/runtime/drivers/firefoxDriver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const { remote } = require('webdriverio');
6 | const { Before } = require('@cucumber/cucumber');
7 | const fs = require('fs-extra');
8 | const path = require('path');
9 |
10 | const apiTagsData = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../scripts/tagList.json')));
11 |
12 | let defaults = {};
13 | let isApiTest;
14 | let useProxy = false;
15 |
16 | Before(async (scenario) => {
17 | try {
18 | const scenarioTags = scenario.pickle.tags.map(tag => tag.name.replace('@', '').toLowerCase());
19 | const tagList = (apiTagsData.tagNames || []).map(tag => tag.replace('@', '').toLowerCase());
20 | isApiTest = scenarioTags.some(tag => tagList.includes(tag));
21 | } catch (error) {
22 | console.error('Error in Before hook: ', error);
23 | }
24 | });
25 |
26 | /**
27 | * create the web browser based on globals set in index.js
28 | * @returns {{}}
29 | */
30 | module.exports = async function firefoxDriver(options) {
31 | defaults = {
32 | logLevel: 'error',
33 | path: '/',
34 | capabilities: {
35 | browserName: 'firefox',
36 | 'moz:firefoxOptions': {
37 | args: ['--disable-popup-blocking', '--disable-gpu'],
38 | },
39 | },
40 | };
41 |
42 | if (isApiTest) {
43 | defaults.capabilities['moz:firefoxOptions'].args.push('--headless', '--disable-popup-blocking', '--disable-gpu');
44 | }
45 |
46 | if (useProxy) {
47 | defaults.capabilities.proxy = {
48 | httpProxy: 'http://klassiarray.klassi.co.uk:8080',
49 | proxyType: 'MANUAL',
50 | autodetect: false,
51 | };
52 | }
53 | const extendedOptions = Object.assign(defaults, options);
54 | global.browser = await remote(extendedOptions);
55 | await global.browser.setWindowSize(1280, 1024);
56 | return global.browser;
57 | };
58 |
--------------------------------------------------------------------------------
/runtime/remotes/lambdatest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const { Before, After } = require('@cucumber/cucumber');
6 |
7 | let scenarioName;
8 | let scenarioResult;
9 |
10 | Before(async (scenario) => {
11 | scenarioName = scenario.pickle.name;
12 | return scenarioName;
13 | });
14 |
15 | After(async (scenario) => {
16 | scenarioResult = scenario.result;
17 | return scenarioResult;
18 | });
19 |
20 | function getCredentials() {
21 | /** adding the ability to deep dive */
22 | const user = process.env.LAMBDATEST_USERNAME;
23 | const key = process.env.LAMBDATEST_ACCESS_KEY;
24 |
25 | assert.isNotEmpty(user, 'lambdatest requires a username');
26 | assert.isNotEmpty(key, 'lambdatest requires an access key');
27 |
28 | return { user, key };
29 | }
30 | let url;
31 | let matchingBuilds;
32 | let sessionsBody;
33 |
34 | async function submitResults() {
35 | const credentials = getCredentials();
36 | const lambdatestUsername = credentials.user;
37 | const lambdatestApiKey = credentials.key;
38 | const apiCredentials = `${lambdatestUsername}:${lambdatestApiKey}`;
39 |
40 | url = `https://${apiCredentials}@api.lambdatest.com/automation/api/v1/builds`;
41 | const buildsBody = await helpers.apiCall(url, 'GET', null, null);
42 | matchingBuilds = buildsBody.body.data;
43 |
44 | let i;
45 | for (i = 0; i < matchingBuilds.length; i++) {
46 | const projectname = matchingBuilds[i].name;
47 | await projectname;
48 | if (projectname === dataconfig.projectName) {
49 | matchingBuilds = matchingBuilds[i].build_id;
50 | }
51 | }
52 | await matchingBuilds;
53 | url = `https://${apiCredentials}@api.lambdatest.com/automation/api/v1/sessions`;
54 | sessionsBody = await helpers.apiCall(url, 'GET', null, null);
55 |
56 | let x;
57 | const sessionData = sessionsBody.body.data;
58 | for (x = 0; x < sessionData.length; x++) {
59 | const sessionName = sessionData[x].build_name;
60 | await sessionName;
61 | }
62 | }
63 |
64 | module.exports = {
65 | submitResults,
66 | getCredentials,
67 | };
68 |
--------------------------------------------------------------------------------
/runtime/drivers/chromeDriver.js:
--------------------------------------------------------------------------------
1 | const { remote } = require('webdriverio');
2 | const { Before } = require('@cucumber/cucumber');
3 | const fs = require('fs-extra');
4 | const path = require('path');
5 |
6 | const apiTagsData = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../scripts/tagList.json')));
7 | const apiTags = global.tagNames;
8 |
9 | let defaults = {};
10 | let isApiTest = false;
11 | let useProxy = false;
12 |
13 | Before(async (scenario) => {
14 | try {
15 | const scenarioTags = scenario.pickle.tags.map(tag => tag.name.replace('@', '').toLowerCase());
16 | const tagList = (apiTagsData.tagNames && apiTags || []).map(tag => tag.replace('@', '').toLowerCase());
17 | isApiTest = scenarioTags.some(tag => tagList.includes(tag));
18 | } catch (error) {
19 | console.error('Error in Before hook: ', error);
20 | }
21 | });
22 |
23 | module.exports = async function chromeDriver(options) {
24 | defaults = {
25 | logLevel: 'error',
26 | path: '/',
27 | automationProtocol: 'webdriver',
28 | capabilities: {
29 | browserName: 'chrome',
30 | 'goog:chromeOptions': {
31 | args: [
32 | '--no-sandbox',
33 | '--disable-gpu',
34 | '--disable-popup-blocking',
35 | '--allow-file-access-from-files',
36 | '--use-fake-device-for-media-stream',
37 | '--use-fake-ui-for-media-stream'
38 | ]
39 | }
40 | }
41 | };
42 |
43 | if (isApiTest) {
44 | defaults.capabilities['goog:chromeOptions'].args.unshift('--headless=new');
45 | }
46 |
47 | if (useProxy) {
48 | defaults.capabilities.proxy = {
49 | httpProxy: 'http://klassiarray.klassi.co.uk:8080',
50 | proxyType: 'MANUAL',
51 | autodetect: false,
52 | };
53 | }
54 |
55 | const extendedOptions = Object.assign(defaults, options);
56 |
57 | try {
58 | global.browser = await remote(extendedOptions);
59 | await global.browser.setWindowSize(1280, 1024);
60 | return global.browser;
61 | } catch (error) {
62 | console.error('Error in Chrome driver:', error.message);
63 | console.error('Stack trace:', error.stack);
64 | throw error;
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/runtime/coding-standards/eslint/eslint.config.js:
--------------------------------------------------------------------------------
1 | const js = require('@eslint/js');
2 | const prettierConfig = require('eslint-config-prettier');
3 | const pluginImport = require('eslint-plugin-import');
4 |
5 | module.exports = [
6 | js.configs.recommended,
7 | {
8 | plugins: {
9 | import: require('eslint-plugin-import'),
10 | prettier: require('eslint-plugin-prettier'),
11 | },
12 |
13 | rules: Object.assign(
14 | {},
15 | js.configs.recommended.rules,
16 | prettierConfig.rules,
17 | pluginImport.configs.errors.rules,
18 | pluginImport.configs.warnings.rules,
19 | {
20 | 'prettier/prettier': [
21 | 'warn',
22 | {
23 | singleQuote: true,
24 | printWidth: 120,
25 | endOfLine: 'auto',
26 | },
27 | ],
28 | 'no-const-assign': 'warn',
29 | 'no-this-before-super': 'warn',
30 | 'no-undef': 'warn',
31 | 'no-unreachable': 'warn',
32 | 'no-unused-vars': 'warn',
33 | 'constructor-super': 'warn',
34 | 'valid-typeof': 'warn',
35 | 'no-console': [0, 'error'],
36 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
37 | indent: ['error', 2, { SwitchCase: 1 }],
38 | semi: ['error', 'always'],
39 | quotes: ['error', 'single'],
40 | },
41 | ),
42 |
43 | languageOptions: {
44 | ecmaVersion: 'latest', // Use latest ECMAScript version
45 | sourceType: 'module',
46 | parserOptions: {
47 | ecmaFeatures: {
48 | dynamicImport: true,
49 | },
50 | },
51 | },
52 | },
53 | {
54 | files: ['**/*.js'],
55 | languageOptions: {
56 | ecmaVersion: 'latest',
57 | sourceType: 'module',
58 | globals: {
59 | Given: true,
60 | When: true,
61 | Then: true,
62 | BeforeStep: true,
63 | After: true,
64 | AfterAll: true,
65 | AfterStep: true,
66 | helpers: true,
67 | browser: true,
68 | env: true,
69 | date: true,
70 | dateTime: true,
71 | startDateTime: true,
72 | endDateTime: true,
73 | browserName: true,
74 | BROWSER_NAME: true,
75 | reportName: true,
76 | projectName: true,
77 | settings: true,
78 | Status: true,
79 | envConfig: true,
80 | assert: true,
81 | expect: true,
82 | console: true,
83 | require: true,
84 | process: true,
85 | module: true,
86 | global: true,
87 | },
88 | },
89 | },
90 | {
91 | files: ['index.js'],
92 | rules: {
93 | 'no-restricted-syntax': 'off',
94 | },
95 | },
96 | ];
97 |
--------------------------------------------------------------------------------
/runtime/drivers/lambdatestDriver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const { remote } = require('webdriverio');
6 | const { Before } = require('@cucumber/cucumber');
7 | const loadConfig = require('../configLoader');
8 | const lambdatest = require('../remotes/lambdatest');
9 | const fs = require('fs-extra');
10 | const path = require('path');
11 |
12 | let config;
13 | let isApiTest;
14 |
15 | const apiTagsData = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../scripts/tagList.json')));
16 | const apiTags = global.tagNames;
17 |
18 | Before(async (scenario) => {
19 | try {
20 | const scenarioTags = scenario.pickle.tags.map(tag => tag.name.replace('@', '').toLowerCase());
21 | const tagList = (apiTagsData.tagNames && apiTags || []).map(tag => tag.replace('@', '').toLowerCase());
22 | isApiTest = scenarioTags.some(tag => tagList.includes(tag));
23 | } catch (error) {
24 | console.error('Error in Before hook: ', error);
25 | }
26 | });
27 |
28 | module.exports = async function lambdatestDriver(options, configType) {
29 | if (configType.length > 1) {
30 | const browserArray = configType.split(',');
31 | for (const browserItem of browserArray) {
32 | await browserExecute(options, browserItem);
33 | }
34 | }
35 | };
36 |
37 | const browserExecute = async (options, configTypeA) => {
38 | const browserCaps = loadConfig(`./lambdatest/${configTypeA}.json`);
39 | const credentials = lambdatest.getCredentials();
40 | const { user, key } = credentials;
41 |
42 | config = browserCaps;
43 |
44 | /** lambdatest will do this anyway, this is to make it explicit */
45 | const buildNameFromConfig = configTypeA.replace(/-/g, ' ');
46 |
47 | if (process.env.CI || process.env.CIRCLE_CI) {
48 | config.tunnelName = process.env.TUNNEL_NAME;
49 | const { CIRCLE_BUILD_NUM, CIRCLE_JOB, CIRCLE_USERNAME } = process.env;
50 | config.build = `${global.projectName} - CircleCI Build No. #${CIRCLE_BUILD_NUM} for ${CIRCLE_USERNAME}. Job: ${CIRCLE_JOB}`;
51 | config.buildTags.push(`${CIRCLE_JOB}`);
52 | } else {
53 | config.build = `${projectName}-${buildNameFromConfig}`;
54 | config.tunnelName = process.env.TUNNEL_NAME || 'klassitunnel';
55 | }
56 |
57 | const capabilities = {
58 | 'LT:Options': {
59 | ...config,
60 | user: user,
61 | accessKey: key,
62 | },
63 | };
64 |
65 | options = {
66 | updateJob: false,
67 | exclude: [],
68 | logLevel: 'silent',
69 | coloredLogs: true,
70 | waitforTimeout: 10000,
71 | connectionRetryTimeout: 90000,
72 | connectionRetryCount: 3,
73 | protocol: 'https',
74 | hostname: 'hub.lambdatest.com',
75 | port: 443,
76 | path: '/wd/hub',
77 | capabilities: capabilities,
78 | };
79 |
80 | try {
81 | global.browser = await remote(options);
82 | } catch (error) {
83 | console.error('Error in lambdatestDriver:', error);
84 | throw error;
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/__tests__/unit/runtime/chromeDriver.js:
--------------------------------------------------------------------------------
1 | jest.mock('webdriverio', () => ({
2 | remote: jest.fn(),
3 | }));
4 |
5 | jest.mock('@cucumber/cucumber', () => ({
6 | Before: jest.fn(),
7 | }));
8 |
9 | jest.mock('../../../cucumber.js', () => ({
10 | filterQuietTags: jest.fn(),
11 | }));
12 |
13 | jest.mock('../../../runtime/drivers/chromeDriver', () => ({
14 | defaults: {
15 | capabilities: { 'goog:chromeOptions': { args: [] } },
16 | },
17 | chromeDriver: jest.fn(),
18 | }));
19 |
20 | const webdriverio = require('webdriverio');
21 | const { chromeDriver, defaults } = require('../../../runtime/drivers/chromeDriver');
22 | const { filterQuietTags } = require('../../../cucumber.js');
23 |
24 | describe('chromeDriver', () => {
25 | let mockBrowser;
26 |
27 | beforeEach(() => {
28 | mockBrowser = { setWindowSize: jest.fn() };
29 | webdriverio.remote.mockResolvedValue(mockBrowser);
30 |
31 | chromeDriver.mockImplementation(async ({ headless }) => {
32 | console.log('✅ chromeDriver function called'); // Debugging
33 |
34 | // Reset args before each test to prevent pollution
35 | defaults.capabilities['goog:chromeOptions'].args = [];
36 |
37 | if (headless) {
38 | defaults.capabilities['goog:chromeOptions'].args.push('--headless', '--disable-extensions');
39 | }
40 |
41 | const browser = await webdriverio.remote({ capabilities: defaults.capabilities });
42 | console.log('✅ webdriverio.remote called'); // Debugging
43 | await browser.setWindowSize(1280, 1024);
44 | console.log('✅ setWindowSize called'); // Debugging
45 | });
46 | });
47 |
48 | it('should run in headless mode when isApiTest is true', async () => {
49 | filterQuietTags.mockResolvedValue(['@api']);
50 |
51 | await chromeDriver({ headless: true });
52 |
53 | console.log('Stubbed Chrome Options:', defaults.capabilities['goog:chromeOptions'].args);
54 |
55 | expect(webdriverio.remote).toHaveBeenCalledWith(
56 | expect.objectContaining({
57 | capabilities: expect.objectContaining({
58 | 'goog:chromeOptions': expect.objectContaining({
59 | args: expect.arrayContaining(['--headless', '--disable-extensions']),
60 | }),
61 | }),
62 | })
63 | );
64 |
65 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
66 | });
67 |
68 | it('should NOT run in headless mode when isApiTest is false', async () => {
69 | filterQuietTags.mockResolvedValue([]);
70 |
71 | await chromeDriver({ headless: false });
72 |
73 | console.log('Stubbed Chrome Options (No Headless):', defaults.capabilities['goog:chromeOptions'].args);
74 |
75 | expect(webdriverio.remote).toHaveBeenCalledWith(
76 | expect.objectContaining({
77 | capabilities: expect.objectContaining({
78 | 'goog:chromeOptions': expect.objectContaining({
79 | args: expect.not.arrayContaining(['--headless']),
80 | }),
81 | }),
82 | })
83 | );
84 |
85 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/__tests__/integration/runtime/chromeDriver.js:
--------------------------------------------------------------------------------
1 | jest.mock('webdriverio', () => ({
2 | remote: jest.fn(),
3 | }));
4 |
5 | jest.mock('@cucumber/cucumber', () => ({
6 | Before: jest.fn(),
7 | }));
8 |
9 | jest.mock('../../../cucumber.js', () => ({
10 | filterQuietTags: jest.fn(),
11 | }));
12 |
13 | jest.mock('../../../runtime/drivers/chromeDriver', () => ({
14 | defaults: {
15 | capabilities: { 'goog:chromeOptions': { args: [] } },
16 | },
17 | chromeDriver: jest.fn(),
18 | }));
19 |
20 | const webdriverio = require('webdriverio');
21 | const { chromeDriver, defaults } = require('../../../runtime/drivers/chromeDriver');
22 | const { filterQuietTags } = require('../../../cucumber.js');
23 |
24 | describe('chromeDriver', () => {
25 | let mockBrowser;
26 |
27 | beforeEach(() => {
28 | mockBrowser = { setWindowSize: jest.fn() };
29 | webdriverio.remote.mockResolvedValue(mockBrowser);
30 |
31 | chromeDriver.mockImplementation(async ({ headless }) => {
32 | console.log('✅ chromeDriver function called'); // Debugging
33 |
34 | // Reset args before each test to prevent pollution
35 | defaults.capabilities['goog:chromeOptions'].args = [];
36 |
37 | if (headless) {
38 | defaults.capabilities['goog:chromeOptions'].args.push('--headless', '--disable-extensions');
39 | }
40 |
41 | const browser = await webdriverio.remote({ capabilities: defaults.capabilities });
42 | console.log('✅ webdriverio.remote called'); // Debugging
43 | await browser.setWindowSize(1280, 1024);
44 | console.log('✅ setWindowSize called'); // Debugging
45 | });
46 | });
47 |
48 | it('should run in headless mode when isApiTest is true', async () => {
49 | filterQuietTags.mockResolvedValue(['@api']);
50 |
51 | await chromeDriver({ headless: true });
52 |
53 | console.log('Stubbed Chrome Options:', defaults.capabilities['goog:chromeOptions'].args);
54 |
55 | expect(webdriverio.remote).toHaveBeenCalledWith(
56 | expect.objectContaining({
57 | capabilities: expect.objectContaining({
58 | 'goog:chromeOptions': expect.objectContaining({
59 | args: expect.arrayContaining(['--headless', '--disable-extensions']),
60 | }),
61 | }),
62 | })
63 | );
64 |
65 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
66 | });
67 |
68 | it('should NOT run in headless mode when isApiTest is false', async () => {
69 | filterQuietTags.mockResolvedValue([]);
70 |
71 | await chromeDriver({ headless: false });
72 |
73 | console.log('Stubbed Chrome Options (No Headless):', defaults.capabilities['goog:chromeOptions'].args);
74 |
75 | expect(webdriverio.remote).toHaveBeenCalledWith(
76 | expect.objectContaining({
77 | capabilities: expect.objectContaining({
78 | 'goog:chromeOptions': expect.objectContaining({
79 | args: expect.not.arrayContaining(['--headless']),
80 | }),
81 | }),
82 | })
83 | );
84 |
85 | expect(mockBrowser.setWindowSize).toHaveBeenCalledWith(1280, 1024);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/runtime/reporter/reporter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const fs = require('fs-extra');
6 | const path = require('path');
7 | const reporter = require('klassijs-cucumber-html-reporter');
8 | const jUnit = require('cucumber-junit');
9 | const pactumJs = require('pactum');
10 |
11 | const s3Upload = require('../s3Upload');
12 | const getRemote = require('../getRemote');
13 | const remoteService = getRemote(settings.remoteService);
14 | const browserName = global.remoteConfig || BROWSER_NAME;
15 |
16 | let resp;
17 | let obj;
18 |
19 | module.exports = {
20 | ipAddr: async () => {
21 | const endPoint = 'http://ip-api.com/json';
22 | resp = await pactumJs.spec().get(endPoint).toss();
23 | await resp;
24 | },
25 |
26 | async reporter() {
27 | const envName = env.envName.toLowerCase();
28 | try {
29 | await this.ipAddr();
30 | obj = await resp.body;
31 | } catch (err) {
32 | obj = {};
33 | console.error('IpAddr func err: ', err.message);
34 | }
35 | if (paths.reports && fs.existsSync(paths.reports)) {
36 | let jsonDir = path.resolve(paths.reports, browserName, envName);
37 | let jsonComDir = path.resolve(paths.reports, browserName, envName + 'Combine');
38 |
39 | global.endDateTime = helpers.getEndDateTime();
40 |
41 | const reportOptions = {
42 | theme: 'hierarchy',
43 | jsonDir: jsonComDir,
44 | output: path.resolve(paths.reports, browserName, envName, `${reportName}-${dateTime}.html`),
45 | reportSuiteAsScenarios: true,
46 | launchReport: !settings.disableReport,
47 | ignoreBadJsonFile: true,
48 | metadata: {
49 | // 'Test Started': startDateTime, TODO: See if i can carry the time from one state to the other
50 | Environment: env.envName,
51 | IpAddress: obj.query,
52 | Browser: browserName,
53 | Location: `${obj.city} ${obj.regionName}`,
54 | Platform: process.platform,
55 | 'Test Completion': endDateTime,
56 | Executed: remoteService && remoteService.type === 'lambdatest' ? 'Remote' : 'Local',
57 | },
58 | brandTitle: `${reportName} ${dateTime}`,
59 | name: `${projectName} ${browserName} ${envName}`,
60 | };
61 | await sleep(DELAY_3s);
62 | // eslint-disable-next-line no-undef
63 | if (!isCI) {
64 | await fs.copySync(jsonDir, jsonComDir);
65 | let jsonfile = path.resolve(paths.reports, browserName, envName + 'Combine', `${reportName}-${dateTime}.json`);
66 | await sleep(DELAY_300ms);
67 | if (resultingString === '@s3load') {
68 | fs.remove(jsonfile, (err) => {
69 | if (err) return console.error(err);
70 | });
71 | await sleep(DELAY_500ms);
72 | await reporter.generate(reportOptions);
73 | await sleep(DELAY_3s).then(async () => {
74 | await s3Upload.s3Upload();
75 | await sleep(DELAY_5s);
76 | });
77 | } else {
78 | await sleep(DELAY_500ms);
79 | await reporter.generate(reportOptions);
80 | }
81 | }
82 | /** grab the file data for xml creation */
83 | let jsonFile = path.resolve(paths.reports, browserName, envName, `${reportName}-${dateTime}.json`);
84 | const reportRaw = fs.readFileSync(jsonFile).toString().trim();
85 |
86 | const xmlReport = jUnit(reportRaw);
87 | const junitOutputPath = path.resolve(
88 | path.resolve(paths.reports, browserName, envName, `${reportName}-${dateTime}.xml`),
89 | );
90 | fs.writeFileSync(junitOutputPath, xmlReport);
91 | }
92 | },
93 | };
94 |
--------------------------------------------------------------------------------
/runtime/s3Upload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const path = require('path');
6 | const fs = require('fs-extra');
7 | const readdir = require('recursive-readdir');
8 | const async = require('async');
9 | const { S3Client, ListBucketsCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
10 | const { astellen } = require('klassijs-astellen');
11 |
12 | /**
13 | * function to upload the test report folder to an s3 Bucket - AWS
14 | */
15 | module.exports = {
16 | s3Upload: async () => {
17 | let envName = env.envName.toLowerCase();
18 | let date = require('./helpers').s3BucketCurrentDate();
19 | const browserName = global.remoteConfig || BROWSER_NAME;
20 | const rootFolder = path.resolve('./reports');
21 | const folderName = `${date}/${dataconfig.s3FolderName}/reports/`;
22 | const BUCKET = s3Data.S3_BUCKET;
23 | const S3_KEY = process.env.S3_KEY;
24 | const S3_SECRET = process.env.S3_SECRET;
25 | const uploadFolder = `./${browserName}/`;
26 |
27 | const s3Client = new S3Client({
28 | region: s3Data.S3_REGION,
29 | credentials: {
30 | accessKeyId: S3_KEY,
31 | secretAccessKey: S3_SECRET,
32 | },
33 | });
34 |
35 | async function mybucketList() {
36 | try {
37 | const data = await s3Client.send(new ListBucketsCommand({}));
38 | return data.Buckets; // For unit tests.
39 | } catch (err) {
40 | console.error('Error ', err.message);
41 | return null; // Return null instead of undefined
42 | }
43 | }
44 |
45 | async function filesToRemove() {
46 | const filePath = rootFolder + '/' + browserName + '/' + envName;
47 | const filelist = fs.readdirSync(filePath);
48 | // console.log('this is the list of files ============> ', filelist);
49 | for (let i = 0; i < filelist.length; i++) {
50 | let filename = filelist[i];
51 |
52 | if (filename.endsWith('.html.json')) {
53 | fs.removeSync(path.resolve(filePath, filename));
54 | }
55 | }
56 | }
57 |
58 | let dirToRemove = rootFolder + '/' + browserName + '/' + envName + 'Combine';
59 | async function getFiles(dirPath) {
60 | if (await fs.existsSync(dirToRemove)) {
61 | await fs.rmSync(dirToRemove, { recursive: true });
62 | }
63 | return fs.existsSync(dirPath) ? readdir(dirPath) : [];
64 | }
65 |
66 | async function deploy(upload) {
67 | await filesToRemove();
68 | const filesToUpload = await getFiles(path.resolve(rootFolder, upload));
69 | await async.eachOfLimit(filesToUpload, 20, async (file) => {
70 | const Key = await file.replace(`${rootFolder}/`, '');
71 | console.log(`uploading: [${Key}]`);
72 | const uploadParams = {
73 | Bucket: BUCKET,
74 | Key: folderName + Key,
75 | Body: fs.readFileSync(file),
76 | };
77 | try {
78 | await s3Client.send(new PutObjectCommand(uploadParams));
79 | } catch (err) {
80 | console.error('Error', err.message);
81 | }
82 | });
83 | }
84 |
85 | const mybucket = await mybucketList();
86 |
87 | // Check if mybucket exists and is an array
88 | if (!mybucket || !Array.isArray(mybucket)) {
89 | console.log('The s3 bucket list could not be retrieved');
90 | return;
91 | }
92 |
93 | const bucketExists = mybucket.some((bucket) => bucket.Name === s3Data.S3_BUCKET);
94 | if (!bucketExists) {
95 | console.log('The s3 bucket does not exist');
96 | return;
97 | }
98 |
99 | try {
100 | await deploy(uploadFolder);
101 | console.log('Report files uploaded successfully to s3 Bucket');
102 | } catch (err) {
103 | console.error(err.message);
104 | }
105 | },
106 | };
107 |
--------------------------------------------------------------------------------
/runtime/utils/klassiOutlook.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const pactumJs = require('pactum');
3 | const qs = require('qs');
4 |
5 | const tenantId = process.env.TENANT_ID;
6 | const clientId = process.env.CLIENT_ID;
7 | const clientSecret = process.env.CLIENT_SECRET;
8 | // const userEmail = 'QAE.TestAccounts@klassi.co.uk'; // This email address is for user with shared mailbox.
9 | const userEmail = 'qaAutoTest@klassi.co.uk'; // This email address is for user with own mailbox.
10 |
11 | const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
12 | const graphEndpoint = `https://graph.microsoft.com/v1.0/users/${userEmail}/messages`;
13 |
14 | let emailData = 'Han.Solo@klassi.co.uk';
15 |
16 | async function getAccessToken() {
17 | const data = {
18 | client_id: clientId,
19 | scope: 'https://graph.microsoft.com/.default',
20 | client_secret: clientSecret,
21 | grant_type: 'client_credentials',
22 | };
23 |
24 | const response = await pactumJs.spec()
25 | .post(tokenEndpoint)
26 | .withHeaders('Content-Type', 'application/x-www-form-urlencoded')
27 | .withBody(qs.stringify(data))
28 | .expectStatus(200);
29 |
30 | const accessToken = response.body.access_token;
31 |
32 | if (!accessToken.includes('.')) {
33 | throw new Error('Invalid token format: Token must be in JWS format (header.payload.signature)');
34 | }
35 | return accessToken;
36 | }
37 |
38 | async function getEmails(accessToken) {
39 | const response = await pactumJs.spec()
40 | .get(`${graphEndpoint}?$top=50`)
41 | .withHeaders({
42 | Authorization: `Bearer ${accessToken}`,
43 | })
44 | .expectStatus(200);
45 |
46 | if (userEmail === 'qaAutoTest@klassi.co.uk') {
47 | return response.body.value.map(email => ({
48 | subject: email.subject,
49 | body: email.body.content,
50 | }));
51 | } else {
52 | return response.body.value.map(email => ({
53 | subject: email.subject,
54 | body: email.body.content,
55 | to: email.toRecipients.map(recipient => recipient.emailAddress.address),
56 | }));
57 | }
58 | }
59 |
60 | (async () => {
61 | let match;
62 | try {
63 | const token = await getAccessToken();
64 | const emails = await getEmails(token);
65 | // console.log( 'Filtered Emails ===================== ' , JSON.stringify(emails, null, 2));
66 | for ( const email of emails) {
67 | if (email.subject.includes('Test Automated Report-01-06-2025-044643')) { // Adjust subject to be generic
68 | console.log('this is the email we are looking for' + email.body);
69 |
70 | match = email.body.match(/\bautomated\b/); // Adjust regex to be more generic
71 | if (match) {
72 | console.log(match[0]);
73 | } else {
74 | console.log('No match found.');
75 | }
76 |
77 | return match[0];
78 |
79 | } else if (Array.isArray(email.to)) {
80 | const recipients = email.to.map(recipient => recipient);
81 | for (const recipient of recipients ) {
82 | if (recipient === emailData) {
83 | console.log('Recipient =========== we are here :');
84 | console.log('this is the email we are looking for === ' + email.subject);
85 | console.log('this is the email we are looking for === ' + email.body);
86 | for (const email of emails) {
87 | if (email.body.includes(5265897)) { // to be generic
88 | match = email.body.match(/\b5265897\b/); // to be generic
89 |
90 | if (match) {
91 | console.log(match[0]);
92 | } else {
93 | console.log('No match found.');
94 | }
95 |
96 | return match[0];
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 | } catch (err) {
104 | console.error('Error:', err.response?.data || err.message);
105 | }
106 | })();
107 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "klassi-js",
3 | "version": "6.1.0",
4 | "description": "A debuggable Javascript testing framework using webdriverio",
5 | "creator": {
6 | "name": "Larry Goddard",
7 | "email": "larryg@klassitech.co.uk",
8 | "X": "https://twitter.com/larryG_01",
9 | "linkedin": "https://linkedin.com/in/larryg",
10 | "youtube": "https://youtube.com/@LarryG_01"
11 | }
12 | ,
13 | "contributors": [
14 | {
15 | "name": "Carlos Bermejo",
16 | "github": "https://github.com/carlosbermejop"
17 | }
18 | ],
19 | "license": "MIT",
20 | "publishConfig": {
21 | "access": "public"
22 | },
23 | "keywords": [
24 | "api testing",
25 | "bdd",
26 | "lambdatest",
27 | "cucumber-js",
28 | "javascript",
29 | "testing framework",
30 | "test automation",
31 | "webdriverio"
32 | ],
33 | "repository": {
34 | "type": "git",
35 | "url": "hhtps://github.com/klassijs/klassi-js"
36 | },
37 | "main": "index.js",
38 | "scripts": {
39 | "dev": "node index.js --disableReport --env test --tags",
40 | "test:checkin": "jest __tests__/unit",
41 | "test:merge": "jest __tests__/integration",
42 | "pkgcheck": "pnpm install --frozen-lockfile",
43 | "preinstall": "npx only-allow pnpm",
44 | "lint": "pnpm lint-staged && pnpm lint:gherkin",
45 | "lint:gherkin": "gherkin-lint -c runtime/coding-standards/gherkin/gherkin-lint.json '**/*.feature' ",
46 | "lint-branch-name": "pnpm branch-name-lint ./branchnamelinter.config.json",
47 | "changelog": "standard-version --skip.commit --skip.tag",
48 | "changelog:patch": "standard-version --release-as patch --skip.commit --skip.tag",
49 | "changelog:minor": "standard-version --release-as minor --skip.commit --skip.tag",
50 | "changelog:major": "standard-version --release-as major --skip.commit --skip.tag"
51 | },
52 | "homepage": "https://github.com/klassijs/klassi-js#readme",
53 | "bugs": {
54 | "url": "https://github.com/klassijs/klassi-js/issues"
55 | },
56 | "dependencies": {
57 | "@aws-sdk/client-s3": "^3.864.0",
58 | "@aws-sdk/client-ses": "^3.864.0",
59 | "@aws-sdk/credential-provider-node": "^3.864.0",
60 | "@cucumber/cucumber": "^11.3.0",
61 | "@cucumber/pretty-formatter": "^1.0.1",
62 | "@eslint/js": "^9.33.0",
63 | "async": "^3.2.6",
64 | "branch-name-lint": "^2.1.1",
65 | "chai": "^5.3.1",
66 | "commander": "^13.1.0",
67 | "cosmiconfig": "^9.0.0",
68 | "cucumber-junit": "^1.7.1",
69 | "dotenv": "^16.6.1",
70 | "eslint": "^9.33.0",
71 | "eslint-config-prettier": "^10.1.8",
72 | "eslint-plugin-import": "^2.32.0",
73 | "eslint-plugin-prettier": "^5.5.4",
74 | "fs-extra": "^11.3.1",
75 | "gherkin-lint": "^4.2.4",
76 | "husky": "^9.1.7",
77 | "is-ci": "^4.1.0",
78 | "klassijs-a11y-validator": "^1.0.0",
79 | "klassijs-astellen": "^1.0.1",
80 | "klassijs-cucumber-html-reporter": "^6.0.0",
81 | "klassijs-smart-ocr": "github:klassijs/klassijs-smart-ocr",
82 | "klassijs-soft-assert": "^1.3.0",
83 | "klassijs-visual-validation": "^1.2.3",
84 | "lint-staged": "^15.5.2",
85 | "merge": "^2.1.1",
86 | "nodemailer": "^6.10.1",
87 | "pactum": "^3.8.0",
88 | "prettier": "^3.6.2",
89 | "recursive-readdir": "^2.2.3",
90 | "require-dir": "^1.2.0",
91 | "standard-version": "github:klassijs/standard-version",
92 | "text-files-loader": "^1.0.5",
93 | "webdriverio": "^9.19.1"
94 | },
95 | "lint-staged": {
96 | "**/*.js": "eslint --quiet --fix --config runtime/coding-standards/eslint/eslint.config.js"
97 | },
98 | "jest-junit": {
99 | "suiteName": "jest tests",
100 | "outputDirectory": "./reports",
101 | "outputName": "junit.xml",
102 | "uniqueOutputName": "false"
103 | },
104 | "devDependencies": {
105 | "@commitlint/cli": "^19.8.1",
106 | "@commitlint/config-conventional": "^19.8.1",
107 | "jest": "^29.7.0",
108 | "jest-junit": "^16.0.0"
109 | }
110 | }
--------------------------------------------------------------------------------
/runtime/s3ReportProcessor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const path = require('path');
6 | const fs = require('fs-extra');
7 | const { S3Client, ListObjectsCommand } = require('@aws-sdk/client-s3');
8 | const program = require('commander');
9 |
10 | const s3Bucket = s3Data.S3_BUCKET;
11 | const s3AccessKeyId = process.env.S3_KEY;
12 | const s3SecretAccessKey = process.env.S3_SECRET;
13 | const domainName = s3Data.S3_DOMAIN_NAME;
14 |
15 | const s3Client = new S3Client({
16 | region: s3Data.S3_REGION,
17 | credentials: {
18 | accessKeyId: s3AccessKeyId,
19 | secretAccessKey: s3SecretAccessKey,
20 | },
21 | });
22 |
23 | module.exports = {
24 | async s3Processor(projectName) {
25 | const s3date = helpers.formatDate();
26 | const folderName = helpers.formatDate();
27 | projectName = dataconfig.s3FolderName;
28 | console.log(`Starting Processing of Test Report for: ${s3date}/${projectName} ...`);
29 | /**
30 | * This creates the test report from the sample template
31 | * @type {string}
32 | */
33 | const tempFile = path.resolve(__dirname, './scripts/s3ReportSample');
34 | let filePath;
35 | let date = helpers.currentDate();
36 | if (program.opts().dlink) {
37 | filePath = `../../${projectName}/test/reports/testReport-${date}.html`;
38 | } else {
39 | filePath = `../${projectName}/reports/testReport-${date}.html`;
40 | }
41 |
42 | const file = filePath;
43 | await fs.copySync(tempFile, file);
44 |
45 | /**
46 | * list of browsers test running on via lambdatest
47 | * @type {string[]}
48 | */
49 | const browserName = ['chrome', 'firefox', 'edge', 'safari', 'tabletGalaxy', 'tabletiPad'];
50 | let dataList;
51 | let dataNew = '';
52 | let browsername;
53 | let dataOut = await helpers.readFromFile(tempFile);
54 |
55 | const bucketParams = {
56 | Bucket: s3Bucket,
57 | Marker: folderName,
58 | Prefix: `${s3date}/${projectName}`,
59 | MaxKeys: 1000,
60 | };
61 |
62 | const data = await s3Client.send(new ListObjectsCommand(bucketParams));
63 | if (data.Contents) {
64 | for (let x = 0; x < browserName.length; x++) {
65 | browsername = browserName[x];
66 | const linkList = [];
67 |
68 | for (let i = 0; i < data.Contents.length; i++) {
69 | const key = data.Contents[i].Key;
70 | if (key.substring(0, 10) === folderName) {
71 | if (key.split('.')[1] === 'html') {
72 | dataList = `${domainName}/${key}`;
73 | if (dataList.includes(browsername)) {
74 | const envDataNew = dataList.replace(/^.*reports\/\w+\//, '').replace(/\/.*.html/, '');
75 | dataNew = dataList
76 | .replace(/^.*reports\/\w+\//, '')
77 | .replace(`${envDataNew}/`, '')
78 | .replace(/\.html/, '');
79 | const theNewData = `${dataNew} -- ${envDataNew}`;
80 | let dataFile = '';
81 | linkList.push((dataFile = `${dataFile}${theNewData} `));
82 | }
83 | }
84 | }
85 | }
86 | if (linkList.length > 0) {
87 | const browserData = `${browsername}
${linkList.join(
88 | ' ',
89 | )}
`;
90 | dataOut = dataOut.replace('<-- browser_test_output -->', browserData);
91 | } else {
92 | dataOut = dataOut.replace('<-- browser_test_output -->', ' ');
93 | }
94 | }
95 | }
96 | await helpers.writeToTxtFile(file, dataOut);
97 | if (dataList === undefined) {
98 | console.error('There is no reporting data for this Project....');
99 | } else if (dataList.length > 0) {
100 | console.log('Test run completed and s3 report being sent .....');
101 | await helpers.klassiEmail();
102 | }
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/__tests__/unit/runtime/s3Upload.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs-extra');
3 | const { S3Client, ListBucketsCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
4 |
5 | // Mock the dependencies
6 | jest.mock('@aws-sdk/client-s3');
7 | jest.mock('fs-extra');
8 | jest.mock('recursive-readdir');
9 | jest.mock('klassijs-astellen');
10 |
11 | // Mock the helpers module
12 | jest.mock('../../../runtime/helpers', () => ({
13 | s3BucketCurrentDate: jest.fn().mockReturnValue('2024-01-01'),
14 | }));
15 |
16 | // Mock the async module
17 | jest.mock('async', () => ({
18 | eachOfLimit: jest.fn().mockImplementation((files, limit, callback) => {
19 | return Promise.resolve();
20 | }),
21 | }));
22 |
23 | // Mock the recursive-readdir module
24 | jest.mock('recursive-readdir', () =>
25 | jest.fn().mockResolvedValue([
26 | './reports/browserName/testenv/file1.html.json',
27 | './reports/browserName/testenv/file2.html.json',
28 | ])
29 | );
30 |
31 | // Mock the global variables and environment
32 | global.env = { envName: 'testenv' };
33 | global.BROWSER_NAME = 'browserName';
34 | global.dataconfig = { s3FolderName: 'mockedFolder' };
35 | global.s3Data = { S3_BUCKET: 'mockBucket', S3_REGION: 'us-east-1' };
36 | process.env.S3_KEY = 'mockKey';
37 | process.env.S3_SECRET = 'mockSecret';
38 |
39 | describe('s3Upload', () => {
40 | let mockS3Client;
41 | let mockSend;
42 |
43 | beforeEach(() => {
44 | // Reset all mocks
45 | jest.clearAllMocks();
46 |
47 | // Mock S3Client
48 | mockSend = jest.fn();
49 | mockS3Client = {
50 | send: mockSend,
51 | };
52 | S3Client.mockImplementation(() => mockS3Client);
53 |
54 | // Mock ListBucketsCommand
55 | ListBucketsCommand.mockImplementation((params) => params);
56 |
57 | // Mock PutObjectCommand
58 | PutObjectCommand.mockImplementation((params) => params);
59 |
60 | // Mock fs methods
61 | fs.existsSync.mockReturnValue(true);
62 | fs.readdirSync.mockReturnValue(['file1.html.json']);
63 | fs.removeSync.mockImplementation(() => {});
64 | fs.ensureDirSync.mockImplementation(() => {});
65 | fs.emptyDirSync.mockImplementation(() => {});
66 | fs.writeFileSync.mockImplementation(() => {});
67 | fs.readFileSync.mockReturnValue('fileContent');
68 | });
69 |
70 | it('should upload files to S3 and use the mocked envConfig and dataConfig', async () => {
71 | // Mock successful bucket list response
72 | mockSend.mockResolvedValueOnce({
73 | Buckets: [{ Name: 'mockBucket' }]
74 | });
75 |
76 | // Mock successful upload
77 | mockSend.mockResolvedValueOnce({});
78 |
79 | const { s3Upload } = require('../../../runtime/s3Upload');
80 | await s3Upload();
81 |
82 | expect(mockS3Client.send).toHaveBeenCalled();
83 | expect(fs.removeSync).toHaveBeenCalledWith(path.resolve('./reports/browserName/testenv/', 'file1.html.json'));
84 | expect(global.dataconfig.s3FolderName).toBe('mockedFolder');
85 | expect(global.dataconfig.s3FolderName).toBeDefined();
86 | });
87 |
88 | it('should print an error message if list of buckets cannot be retrieved', async () => {
89 | const logSpy = jest.spyOn(console, 'log');
90 |
91 | // Mock failed bucket list
92 | mockSend.mockRejectedValueOnce(new Error('Test Error'));
93 |
94 | const { s3Upload } = require('../../../runtime/s3Upload');
95 | await s3Upload();
96 |
97 | expect(logSpy).toHaveBeenCalledWith('The s3 bucket list could not be retrieved');
98 | logSpy.mockRestore();
99 | });
100 |
101 | it('should remove a combine directory if it already exists', async () => {
102 | const testFolderPath = path.resolve(process.cwd(), 'reports', 'browserName', 'testenvCombine');
103 |
104 | // Mock successful bucket list response
105 | mockSend.mockResolvedValueOnce({
106 | Buckets: [{ Name: 'mockBucket' }]
107 | });
108 |
109 | // Mock successful upload
110 | mockSend.mockResolvedValueOnce({});
111 |
112 | // Mock fs.rmSync to simulate directory removal
113 | fs.rmSync.mockImplementation(() => {});
114 |
115 | fs.ensureDirSync(testFolderPath);
116 |
117 | const { s3Upload } = require('../../../runtime/s3Upload');
118 | await s3Upload();
119 |
120 | // Verify that fs.rmSync was called (which removes the combine directory)
121 | expect(fs.rmSync).toHaveBeenCalled();
122 |
123 | try {
124 | fs.emptyDirSync(path.resolve(process.cwd(), 'reports', 'browserName'));
125 | } catch (err) {
126 | // Ignore cleanup errors
127 | }
128 | });
129 |
130 | it('should collect the files to be uploaded', async () => {
131 | const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
132 | const testFolderPath = path.resolve(process.cwd(), 'reports', 'browserName');
133 |
134 | // Mock successful bucket list response
135 | mockSend.mockResolvedValueOnce({
136 | Buckets: [{ Name: 'mockBucket' }]
137 | });
138 |
139 | // Mock successful upload
140 | mockSend.mockResolvedValueOnce({});
141 |
142 | fs.ensureDirSync(testFolderPath);
143 | fs.writeFileSync(path.resolve(testFolderPath, 'testfile1.txt'), 'Test data');
144 |
145 | const { s3Upload } = require('../../../runtime/s3Upload');
146 | await s3Upload();
147 |
148 | try {
149 | expect(logSpy).toHaveBeenCalledWith('Report files uploaded successfully to s3 Bucket');
150 | } catch (err) {
151 | throw new Error(err);
152 | } finally {
153 | fs.emptyDirSync(testFolderPath);
154 | logSpy.mockRestore();
155 | }
156 | });
157 |
158 | it('should fail gracefully if an error occurs while uploading to the S3 bucket', async () => {
159 | const testFolderPath = path.resolve(process.cwd(), 'reports', 'browserName');
160 |
161 | // Mock successful bucket list response first
162 | mockSend.mockResolvedValueOnce({
163 | Buckets: [{ Name: 'mockBucket' }]
164 | });
165 |
166 | // Mock successful bucket list for the second call (in deploy)
167 | mockSend.mockResolvedValueOnce({
168 | Buckets: [{ Name: 'mockBucket' }]
169 | });
170 |
171 | // Mock failed upload - this will trigger the error in the deploy function
172 | mockSend.mockRejectedValueOnce(new Error('Test Error'));
173 |
174 | fs.ensureDirSync(testFolderPath);
175 | fs.writeFileSync(path.resolve(testFolderPath, 'testfile1.txt'), 'Test data');
176 |
177 | const { s3Upload } = require('../../../runtime/s3Upload');
178 |
179 | // The function should complete without throwing an error
180 | await expect(s3Upload()).resolves.not.toThrow();
181 |
182 | try {
183 | fs.emptyDirSync(testFolderPath);
184 | } catch (err) {
185 | // Ignore cleanup errors
186 | }
187 | });
188 | });
189 |
190 |
--------------------------------------------------------------------------------
/__tests__/integration/runtime/s3Upload.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs-extra');
3 | const { S3Client, ListBucketsCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
4 |
5 | // Mock the dependencies
6 | jest.mock('@aws-sdk/client-s3');
7 | jest.mock('fs-extra');
8 | jest.mock('recursive-readdir');
9 | jest.mock('klassijs-astellen');
10 |
11 | // Mock the helpers module
12 | jest.mock('../../../runtime/helpers', () => ({
13 | s3BucketCurrentDate: jest.fn().mockReturnValue('2024-01-01'),
14 | }));
15 |
16 | // Mock the async module
17 | jest.mock('async', () => ({
18 | eachOfLimit: jest.fn().mockImplementation((files, limit, callback) => {
19 | return Promise.resolve();
20 | }),
21 | }));
22 |
23 | // Mock the recursive-readdir module
24 | jest.mock('recursive-readdir', () =>
25 | jest.fn().mockResolvedValue([
26 | './reports/browserName/testenv/file1.html.json',
27 | './reports/browserName/testenv/file2.html.json',
28 | ])
29 | );
30 |
31 | // Mock the global variables and environment
32 | global.env = { envName: 'testenv' };
33 | global.BROWSER_NAME = 'browserName';
34 | global.dataconfig = { s3FolderName: 'mockedFolder' };
35 | global.s3Data = { S3_BUCKET: 'mockBucket', S3_REGION: 'us-east-1' };
36 | process.env.S3_KEY = 'mockKey';
37 | process.env.S3_SECRET = 'mockSecret';
38 |
39 | describe('s3Upload', () => {
40 | let mockS3Client;
41 | let mockSend;
42 |
43 | beforeEach(() => {
44 | // Reset all mocks
45 | jest.clearAllMocks();
46 |
47 | // Mock S3Client
48 | mockSend = jest.fn();
49 | mockS3Client = {
50 | send: mockSend,
51 | };
52 | S3Client.mockImplementation(() => mockS3Client);
53 |
54 | // Mock ListBucketsCommand
55 | ListBucketsCommand.mockImplementation((params) => params);
56 |
57 | // Mock PutObjectCommand
58 | PutObjectCommand.mockImplementation((params) => params);
59 |
60 | // Mock fs methods
61 | fs.existsSync.mockReturnValue(true);
62 | fs.readdirSync.mockReturnValue(['file1.html.json']);
63 | fs.removeSync.mockImplementation(() => {});
64 | fs.ensureDirSync.mockImplementation(() => {});
65 | fs.emptyDirSync.mockImplementation(() => {});
66 | fs.writeFileSync.mockImplementation(() => {});
67 | fs.readFileSync.mockReturnValue('fileContent');
68 | });
69 |
70 | it('should upload files to S3 and use the mocked envConfig and dataConfig', async () => {
71 | // Mock successful bucket list response
72 | mockSend.mockResolvedValueOnce({
73 | Buckets: [{ Name: 'mockBucket' }]
74 | });
75 |
76 | // Mock successful upload
77 | mockSend.mockResolvedValueOnce({});
78 |
79 | const { s3Upload } = require('../../../runtime/s3Upload');
80 | await s3Upload();
81 |
82 | expect(mockS3Client.send).toHaveBeenCalled();
83 | expect(fs.removeSync).toHaveBeenCalledWith(path.resolve('./reports/browserName/testenv/', 'file1.html.json'));
84 | expect(global.dataconfig.s3FolderName).toBe('mockedFolder');
85 | expect(global.dataconfig.s3FolderName).toBeDefined();
86 | });
87 |
88 | it('should print an error message if list of buckets cannot be retrieved', async () => {
89 | const logSpy = jest.spyOn(console, 'log');
90 |
91 | // Mock failed bucket list
92 | mockSend.mockRejectedValueOnce(new Error('Test Error'));
93 |
94 | const { s3Upload } = require('../../../runtime/s3Upload');
95 | await s3Upload();
96 |
97 | expect(logSpy).toHaveBeenCalledWith('The s3 bucket list could not be retrieved');
98 | logSpy.mockRestore();
99 | });
100 |
101 | it('should remove a combine directory if it already exists', async () => {
102 | const testFolderPath = path.resolve(process.cwd(), 'reports', 'browserName', 'testenvCombine');
103 |
104 | // Mock successful bucket list response
105 | mockSend.mockResolvedValueOnce({
106 | Buckets: [{ Name: 'mockBucket' }]
107 | });
108 |
109 | // Mock successful upload
110 | mockSend.mockResolvedValueOnce({});
111 |
112 | // Mock fs.rmSync to simulate directory removal
113 | fs.rmSync.mockImplementation(() => {});
114 |
115 | fs.ensureDirSync(testFolderPath);
116 |
117 | const { s3Upload } = require('../../../runtime/s3Upload');
118 | await s3Upload();
119 |
120 | // Verify that fs.rmSync was called (which removes the combine directory)
121 | expect(fs.rmSync).toHaveBeenCalled();
122 |
123 | try {
124 | fs.emptyDirSync(path.resolve(process.cwd(), 'reports', 'browserName'));
125 | } catch (err) {
126 | // Ignore cleanup errors
127 | }
128 | });
129 |
130 | it('should collect the files to be uploaded', async () => {
131 | const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
132 | const testFolderPath = path.resolve(process.cwd(), 'reports', 'browserName');
133 |
134 | // Mock successful bucket list response
135 | mockSend.mockResolvedValueOnce({
136 | Buckets: [{ Name: 'mockBucket' }]
137 | });
138 |
139 | // Mock successful upload
140 | mockSend.mockResolvedValueOnce({});
141 |
142 | fs.ensureDirSync(testFolderPath);
143 | fs.writeFileSync(path.resolve(testFolderPath, 'testfile1.txt'), 'Test data');
144 |
145 | const { s3Upload } = require('../../../runtime/s3Upload');
146 | await s3Upload();
147 |
148 | try {
149 | expect(logSpy).toHaveBeenCalledWith('Report files uploaded successfully to s3 Bucket');
150 | } catch (err) {
151 | throw new Error(err);
152 | } finally {
153 | fs.emptyDirSync(testFolderPath);
154 | logSpy.mockRestore();
155 | }
156 | });
157 |
158 | it('should fail gracefully if an error occurs while uploading to the S3 bucket', async () => {
159 | const testFolderPath = path.resolve(process.cwd(), 'reports', 'browserName');
160 |
161 | // Mock successful bucket list response first
162 | mockSend.mockResolvedValueOnce({
163 | Buckets: [{ Name: 'mockBucket' }]
164 | });
165 |
166 | // Mock successful bucket list for the second call (in deploy)
167 | mockSend.mockResolvedValueOnce({
168 | Buckets: [{ Name: 'mockBucket' }]
169 | });
170 |
171 | // Mock failed upload - this will trigger the error in the deploy function
172 | mockSend.mockRejectedValueOnce(new Error('Test Error'));
173 |
174 | fs.ensureDirSync(testFolderPath);
175 | fs.writeFileSync(path.resolve(testFolderPath, 'testfile1.txt'), 'Test data');
176 |
177 | const { s3Upload } = require('../../../runtime/s3Upload');
178 |
179 | // The function should complete without throwing an error
180 | await expect(s3Upload()).resolves.not.toThrow();
181 |
182 | try {
183 | fs.emptyDirSync(testFolderPath);
184 | } catch (err) {
185 | // Ignore cleanup errors
186 | }
187 | });
188 | });
189 |
190 |
--------------------------------------------------------------------------------
/runtime/world.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const { setDefaultTimeout, Before, AfterAll } = require('@cucumber/cucumber');
6 | const { astellen } = require('klassijs-astellen');
7 | const getRemote = require('./getRemote');
8 | const data = require('./helpers');
9 | const { filterQuietTags } = require('../cucumber');
10 | const { getTagsFromFeatureFiles } = require('../index');
11 | const { throwCollectedErrors } = require('klassijs-soft-assert');
12 | const { ImageAssertion } = require('klassijs-visual-validation');
13 |
14 | /**
15 | * This is the Global date functionality
16 | */
17 | global.date = data.currentDate();
18 |
19 | /**
20 | * Driver environment variables
21 | * @type {function(*): {}}
22 | */
23 | // const MultiDriver = require('./drivers/multiDriver');
24 | const ChromeDriver = require('./drivers/chromeDriver');
25 | const FirefoxDriver = require('./drivers/firefoxDriver');
26 | const LambdaTestDriver = require('./drivers/lambdatestDriver');
27 |
28 | const remoteService = getRemote(global.settings.remoteService);
29 |
30 | let driver = {};
31 | global.world = this;
32 | /**
33 | * create the web browser based on global let set in index.js
34 | * @returns {{}}
35 | */
36 | async function getDriverInstance() {
37 | let browser = global.BROWSER_NAME;
38 |
39 | astellen.set('BROWSER_NAME', global.BROWSER_NAME);
40 | const options = {};
41 | if (remoteService && remoteService.type === 'lambdatest') {
42 | astellen.set('BROWSER_NAME', global.settings.extraSettings);
43 | console.log('extraSettings via astellen GET', astellen.get('BROWSER_NAME'));
44 | const configType = global.remoteConfig;
45 | assert.isString(configType, 'LambdaTest requires a config type e.g. browserName.json');
46 | driver = LambdaTestDriver(options, configType);
47 | global.browser = driver;
48 | return driver;
49 | }
50 | assert.isNotEmpty(browser, 'Browser must be defined');
51 |
52 | const getBrowser = {
53 | firefox: async() => {
54 | driver = await FirefoxDriver(options);
55 | return driver;
56 | },
57 | chrome: async() => {
58 | driver = await ChromeDriver(options);
59 | return driver;
60 | },
61 | default: async() => {
62 | driver = await ChromeDriver(options);
63 | return driver;
64 | },
65 | };
66 | await (getBrowser[browser] || getBrowser['default'])();
67 | global.browser = driver;
68 | return driver;
69 | }
70 |
71 | /**
72 | * set the default timeout for all tests
73 | */
74 | const globalTimeout = process.env.CUCUMBER_TIMEOUT || 300000;
75 | setDefaultTimeout(globalTimeout);
76 | global.timeout = globalTimeout;
77 |
78 | /**
79 | * create the browser before scenario if it's not instantiated and
80 | * also exposing the world object in global variable 'cucumberThis' so that
81 | * it can be used in arrow functions
82 | */
83 | let cucumberThis;
84 |
85 | Before(async function () {
86 | global.cucumberThis = this;
87 | cucumberThis = this;
88 | global.driver = await getDriverInstance();
89 | global.browser = global.driver;
90 | return driver;
91 | });
92 |
93 | /**
94 | * start recording of the Test run time
95 | */
96 | global.startDateTime = data.getStartDateTime();
97 |
98 | /**
99 | * executed before each scenario
100 | */
101 | Before(async (scenario) => {
102 | const { browser } = global;
103 | if (remoteService && remoteService.type === 'lambdatest') {
104 | await browser.execute(`lambda-name=${scenario.pickle.name}`);
105 | }
106 | });
107 |
108 | /**
109 | * This verifies that the current scenario to be run includes the @wip or @skip tags
110 | * and skips the test if that's the case.
111 | */
112 | Before((scenario) => {
113 | const correctMultipleTags = module.exports.skipTagValidation();
114 | for (const tag of scenario.pickle.tags) {
115 | if (
116 | tag.name === '@wip' ||
117 | tag.name === '@skip' ||
118 | (correctMultipleTags && correctMultipleTags.includes(tag.name))
119 | ) {
120 | cucumberThis.attach(
121 | `This scenario was skipped automatically by using the @wip, @skip or a custom tag "${tag.name}" provided at runtime.`,
122 | );
123 | return 'skipped';
124 | }
125 | }
126 | });
127 |
128 | /**
129 | * LambdaTest Only
130 | * executed ONLY on failure of a scenario to get the video link
131 | * from lambdatest when it fails for the report
132 | */
133 | After(async (scenario) => {
134 | if (scenario.result.status === Status.FAILED && remoteService && remoteService.type === 'lambdatest') {
135 | await helpers.ltVideo();
136 | // eslint-disable-next-line no-undef
137 | const vidLink = await videoLib.getVideoId();
138 | cucumberThis.attach(
139 | `video:\n `,
140 | );
141 | }
142 | });
143 |
144 | /**
145 | * This is to control closing the browser or keeping it open after each scenario
146 | * @returns {Promise|*}
147 | */
148 | async function browserOpen() {
149 | const { browser } = global;
150 | if (typeof global.browser === 'undefined') {
151 | console.warn('Browser is not defined, skipping cleanup');
152 | return Promise.resolve();
153 | }
154 |
155 | if (global.browserOpen === false) {
156 | return browser.deleteSession();
157 | } else {
158 | return Promise.resolve();
159 | }
160 | };
161 |
162 | /**
163 | * executed after each scenario - always closes the browser to ensure clean browser not cached)
164 | */
165 | After(async (scenario) => {
166 | if (
167 | scenario.result.status === Status.FAILED ||
168 | scenario.result.status === Status.PASSED ||
169 | scenario.result.status === Status.SKIPPED ||
170 | scenario.result.status === Status.UNKNOWN ||
171 | scenario.result.status === Status.AMBIGUOUS ||
172 | scenario.result.status === Status.UNDEFINED ||
173 | scenario.result.status === Status.PENDING
174 | ) {
175 | if (remoteService && remoteService.type === 'lambdatest') {
176 | if (scenario.result.status === 'FAILED') {
177 | await browser.execute('lambda-status=failed');
178 | } else if (scenario.result.status === Status.PASSED) {
179 | await browser.execute('lambda-status=passed');
180 | } else if (scenario.result.status === Status.SKIPPED) {
181 | await browser.execute('lambda-status=skipped');
182 | } else if (scenario.result.status === Status.UNKNOWN) {
183 | await browser.execute('lambda-status=unknown');
184 | } else if (scenario.result.status === Status.AMBIGUOUS) {
185 | await browser.execute('lambda-status=ignored');
186 | } else if (scenario.result.status === Status.UNDEFINED) {
187 | await browser.execute('lambda-status=error');
188 | } else if (scenario.result.status === Status.PENDING) {
189 | await browser.execute('lambda-status=skipped');
190 | }
191 | return await browserOpen();
192 | }
193 | }
194 | return await browserOpen();
195 | });
196 |
197 | After(async function () {
198 | // Pass the total assertion errors after each scenario to the report
199 | await throwCollectedErrors();
200 | });
201 |
202 | After(async function () {
203 | // Passing the total visual validation errors after each scenario to the report
204 | await ImageAssertion.finalizeTest();
205 | });
206 |
207 | /**
208 | * get executed only if there is an error within a scenario
209 | * will not take an image if it's an API test
210 | */
211 | After(async function (scenario) {
212 | const world = this;
213 | let result = await filterQuietTags();
214 | const taglist = resultingString.split(',');
215 | if (!taglist.some((tag) => result.includes(tag)) && scenario.result.status === Status.FAILED) {
216 | if (typeof global.browser !== 'undefined' && global.browser.takeScreenshot) {
217 | return await global.browser.takeScreenshot().then((screenShot) => {
218 | // screenShot is a base-64 encoded PNG
219 | world.attach(screenShot, 'image/png');
220 | });
221 | }
222 | }
223 | });
224 |
225 | /**
226 | * this allows for the skipping of scenarios based on tags
227 | * @returns {*|null}
228 | */
229 | function skipTagValidation() {
230 | let multipleTags;
231 | if (!skipTag || skipTag.length === 0) {
232 | return null;
233 | }
234 |
235 | const correctFeatureTags = getTagsFromFeatureFiles();
236 | multipleTags = skipTag.split(',');
237 | const correctTags = [];
238 | for (const tag of multipleTags) {
239 | if (!tag || tag.length === 0) {
240 | continue;
241 | }
242 | if (tag[0] !== '@') {
243 | console.error(`Error: the tag should start with an @ symbol. The skipTag provided was "${tag}". `);
244 | continue;
245 | }
246 | if (!correctFeatureTags.includes(tag)) {
247 | console.error('Error: the requested tag does not exist ===> ', tag);
248 | continue;
249 | }
250 | correctTags.push(tag);
251 | }
252 | return correctTags.length !== 0 ? correctTags : null;
253 | }
254 |
255 | module.exports = { skipTagValidation };
256 |
--------------------------------------------------------------------------------
/runtime/docs/klassi-js-v6-Upgrade.md:
--------------------------------------------------------------------------------
1 | # Klassi-js v6.0.0 Upgrade Guide
2 | # Overview
3 |
4 | This document provides a comprehensive guide to upgrading a project to Klassi-js v6.0.0. The process involves updating assertion types, modifying how assertions are used, upgrading dependencies, and making necessary configuration changes.
5 |
6 | By following this guide, you ensure that your project remains compatible with Klassi-js v6.0.0 while maintaining best practices.
7 |
8 | ## Assertion Types
9 |
10 | Klassi-js v6.0.0 introduces a structured set of assertion types that should be used for validation in your project. Documentation is here. Below is a complete list of valid assertion types:
11 |
12 | Table : Valid Assertion Types
13 |
14 | | Assertion Types | Assertion Types | Assertion Types | Assertion Types |
15 | |:----------------|:----------------|:----------------|:----------------|
16 | | equals | tobeenabled | tobeclickable | isOK |
17 | | tohavetitle | isNull | toexist | isArray |
18 | | contains | tobeselected | isTrue | exists |
19 | | tohaveurl | notExists | match | isdisabled |
20 | | doesexist | tobechecked | isenabled | include |
21 | | tohavetext | isUndefined | tobeexisting | equal |
22 | | doesnotexist | tohavehtml | isFalse | tobedisplayed |
23 | | containstext | isString | lengthOf | tobepresent |
24 | | isnotenabled | tobefocused | notEqual | tobedisabled |
25 | | toNotEqual | typeOf | notInclude | doesnotcontain |
26 |
27 |
28 | ## Why Update Assertions?
29 | Klassi-js v6.0.0 introduces softAssert, which provides better error handling and allows test execution to continue even if an assertion fails. All failures will be shown/reported at the end of the test run.
30 |
31 | #### Key Differences:
32 |
33 | softAssert is now used instead of assert
34 | Assertion types must be passed as strings (e.g., 'isTrue', 'equal')
35 | The format improves readability and test maintainability
36 |
37 | Old Format (Before Upgrade, in previous versions, assertions were written using assert):
38 | ```javascript
39 | await assert.isTrue(await var1.includes(testData.errorMessage.invalidISBN));
40 |
41 | await assert.equal(await elem.getText(), testData.userRole.editorRole);
42 | ```
43 | New Format (After Upgrade to Klassi-js v6.0.0, assertions should be written using softAssert):
44 | ```javascript
45 | await softAssert(await var1.includes(testData.errorMessage.invalidISBN), 'isTrue');
46 |
47 | await softAssert(await elem.getText(), 'equal', testData.userRole.editorRole);
48 | ```
49 |
50 | # Visual-Validation Usage Updates
51 | ### Why Update Visual Validation?
52 |
53 | Klassi-js v6.0.0 upgrades visual-validation, which provides better error handling and allows test execution to continue even if an image validation fails. There is no update needed for existing code base its just works out of the box. All failures will be shown / reported at the end of the test run.
54 |
55 |
56 | # Project Upgrade Steps
57 | ### Create a New Branch
58 |
59 | Before making any changes, create a new branch from develop to manage the upgrade process.
60 |
61 | Commands:
62 |
63 | ```javascript
64 | git checkout develop
65 |
66 | git pull origin develop
67 |
68 | git checkout -b upgrade-Klassi-js-v6
69 | ```
70 |
71 | This ensures that your changes are isolated and do not affect the main development branch until they are fully tested and reviewed.
72 |
73 | ### Remove Outdated Files
74 |
75 | Certain files need to be deleted before proceeding with the upgrade:
76 |
77 | ```javascript
78 | rm -rf yarn.lock node_modules .eslintrc
79 | ```
80 | Why?
81 |
82 |
83 | yarn.lock: Removing this ensures fresh dependencies are installed.
84 | node_modules: Deleting this prevents conflicts with older versions.
85 | .eslintrc: The linting configuration might be outdated and needs to be refreshed.
86 |
87 |
88 | ### Updating package.json
89 |
90 | The package.json file should be updated to include the latest dependencies and scripts required for Klassi-js v6.0.0. you can just copy the package.json from Klassijs-example-project taking care to make a note of dependencies that you are using in your project that’s not a part of Klassi-js
91 |
92 | Key Changes:
93 |
94 | Update dependencies and scripts to match Klassijs-example-project.
95 |
96 | Add new contributors if applicable.
97 |
98 | ```json
99 |
100 | "version": "6.0.0",
101 | "license": "MIT",
102 | "creator": {
103 | "name": "Larry Goddard",
104 | "email": "larryg@klassitech.co.uk",
105 | "linkedin": "https://linkedin.com/in/larryg",
106 | "youtube": "https://youtube.com/@LarryG_01"
107 | },
108 |
109 | "engines": {
110 | "node": ">=20",
111 | "pnpm": ">=9"
112 | },
113 |
114 | "scripts": {
115 | "dev": "node ./node_modules/Klassi-js/index.js --disableReport --tags",
116 | "ltlocal": "node ./node_modules/Klassi-js/index.js --disableReport --remoteService lambdatest --extraSettings",
117 | "predev": "pnpm delete-dev",
118 | "delete-dev": "rimraf ./reports ./artifacts ./visual-regression-baseline eng.traineddata",
119 | "============================================": "=======================================================",
120 | "================ NOTE": "PLEASE DO NOT MAKE ANY CHANGES TO THE SCRIPTS BELOW THIS LINE ================",
121 | "===========================================": "========================================================",
122 | "preinstall": "npx only-allow pnpm",
123 | "pkgcheck": "pnpm install --frozen-lockfile",
124 | "lint": "pnpm lint-staged && pnpm lint:gherkin",
125 | "lint:gherkin": "gherkin-lint -c node_modules/Klassi-js/runtime/coding-standards/gherkin/gherkin-lint.json '**/*.feature'",
126 | "lint-branch-name": "pnpm branch-name-lint ./branchnamelinter.config.json",
127 | "ciltuat": "node ./node_modules/Klassi-js/index.js --disableReport --isCI --tags @regression --remoteService lambdatest --extraSettings",
128 | "ciltdev": "node ./node_modules/Klassi-js/index.js --disableReport --isCI --tags @integration --remoteService lambdatest --extraSettings",
129 | "cilts3r": "node ./node_modules/Klassi-js/index.js --disableReport --tags @s3 --remoteService lambdatest --extraSettings",
130 | "cilts3load": "node ./node_modules/Klassi-js/index.js --disableReport --tags @s3load --remoteService lambdatest --extraSettings"
131 | },
132 |
133 | "dependencies": {
134 | "Klassi-js": "github:klassijs/Klassi-js",
135 | "klassijs-astellen": "^1.0.0",
136 | "klassijs-soft-assert": "^1.2.1"
137 | },
138 |
139 | "devDependencies": {
140 | "@commitlint/cli": "^19.8.0",
141 | "@commitlint/config-conventional": "^19.8.0",
142 | "branch-name-lint": "^2.1.1",
143 | "gherkin-lint": "^4.2.4",
144 | "husky": "^9.1.7",
145 | "lint-staged": "^15.5.0",
146 | "rimraf": "^6.0.1"
147 | },
148 |
149 | "lint-staged": {
150 | "**/*.js": "eslint --quiet --fix --config node_modules/Klassi-js/runtime/coding-standards/eslint/eslint.config.js"
151 | }
152 | ```
153 |
154 | Ensure All Dependencies Are Installed
155 | If you have any additional dependencies, ensure they are re-added.
156 |
157 | Example:
158 | "form-data": "^4.0.2"
159 |
160 | ### Updating branchnamelinter.config.json
161 |
162 | Ensure the file enforces proper branch naming conventions.
163 |
164 | New Configuration
165 |
166 | ```json
167 | {
168 | "branchNameLinter": {
169 | "prefixes": [
170 | "testfix",
171 | "automation",
172 | "feature"
173 | ],
174 | "suggestions": {
175 | "fix": "testfix",
176 | "auto": "automation",
177 | "feat": "feature"
178 | },
179 | "disallowed": [
180 | "master",
181 | "main",
182 | "develop",
183 | "staging"
184 | ],
185 | "separator": "/",
186 | "msgBranchBanned": "Branches with the name \"%s\" are not allowed.",
187 | "msgBranchDisallowed": "Pushing to \"%s\" is not allowed, use git-flow.",
188 | "msgPrefixNotAllowed": "Branch prefix \"%s\" is not allowed.",
189 | "msgPrefixSuggestion": "Instead of \"%s\" try \"%s\".",
190 | "msgseparatorRequired": "Branch \"%s\" must contain a separator \"%s\"."
191 | }
192 | }
193 | ```
194 | This helps maintain consistency in branch naming and avoids using restricted branch names.
195 |
196 |
197 | ### Updating .gitigmore
198 |
199 | Ensure the .gitignore file includes all necessary exclusions:
200 |
201 | ```gitignore
202 | # Dependency directories
203 | node_modules
204 | log
205 |
206 | # Files and Directory
207 | # project folders
208 | reports
209 | artifacts
210 |
211 | # IntelliJ project files
212 | .idea
213 | .DS_Store
214 | out
215 | gen
216 |
217 | # vscode files
218 | .vscode
219 |
220 | # Logs
221 | pnpm-debug.log*
222 |
223 | # dotenv environment variables file
224 | .env
225 | .env.*
226 |
227 | # OCR file
228 | eng.traineddata
229 | ```
230 |
231 | This prevents unnecessary files from being committed to the repository.
232 |
233 | ### Updating Husky Hooks (.husky folder)
234 |
235 | Husky helps enforce code quality by running checks before committing.
236 |
237 | Commit Message Hook (commit-msg)
238 |
239 | ```sh
240 | #!/usr/bin/env sh
241 | npx --no -- commitlint --edit "${1}"
242 | pnpm lint-branch-name
243 | ```
244 |
245 | Pre-Commit Hook (pre-commit)
246 | ```sh
247 | #!/usr/bin/env sh
248 | pnpm lint
249 | ```
250 |
251 | These scripts ensure commits follow proper formatting and branch names comply with naming rules.
252 |
253 | ### Updating CircleCI Configuration
254 |
255 | Replace .circleci/config.yml with the latest version from:
256 | please make a note of your project cron job time and replace 'projectName' with your project GitHub name.
257 | ```shell
258 | Klassijs/klassijs-example-project/.circleci/config.yml.template
259 | ```
260 | This ensures that the CI/CD pipeline remains up to date.
261 |
262 | ### Updating LambdaTest Browser Versions
263 | Update the browser versions in the browser configuration file with the latest-2 version number (NOTE: use actual numbers i.e '132' and not 'latest-2')
264 | ```shell
265 | Ensure to update the browser versions to the latest-2 in the folder.
266 |
267 | cp -r Klassijs/Klassijs-example-project/lambdatest ./lambdatest
268 | ```
269 | ### Upgrading Node.js Version
270 |
271 | Ensure your project is using the required Node.js version:
272 | ```shell
273 | node -v
274 | ```
275 | If needed, install and use the correct version:
276 | nvm install 20
277 | nvm use 20
278 |
279 | ### Installing Dependencies
280 |
281 | Once all changes have been made, install dependencies:
282 | ```shell
283 | pnpm install
284 | ```
285 | This ensures all required packages are properly set up.
286 |
287 | ### Running the Updated Project
288 |
289 | Local Execution
290 | Run the project locally using:
291 | ```shell
292 | pnpm dev @tagname
293 | ```
294 | LambdaTest Execution
295 | To run tests in LambdaTest:
296 | ```shell
297 | pnpm ltlocal browserName --tags @tagName
298 | ```
299 |
300 | ### Assertion upgrades
301 |
302 | This will take a while so it can be done over time, please go back to Point 2 and follow the instructions there. Read the README for [klassijs-soft-assert](https://github.com/klassijs/klassijs-soft-assert) so you can update all your assertions in your existing code.
303 |
304 | ### NOTE:
305 |
306 | By following this structured guide, your project will be successfully upgraded to Klassi-js v6.0.0.
307 | This ensures improved assertion handling, an optimized test framework, and a well-maintained codebase.
308 | For any issues, compare your changes with [klassijs-example-project](https://github.com/klassijs/Klassijs-example-project) or seek help from the contributors.
309 |
--------------------------------------------------------------------------------
/runtime/mailer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 |
6 | // const path = require('path');
7 | // const nodemailer = require('nodemailer');
8 | // const { SESClient } = require('@aws-sdk/client-ses');
9 | // const { defaultProvider } = require('@aws-sdk/credential-provider-node');
10 | // const getRemote = require('./getRemote');
11 | // const { astellen } = require('klassijs-astellen');
12 | //
13 | // const remoteService = getRemote(settings.remoteService);
14 | // const emailDateTime = helpers.emailReportDateTime();
15 | //
16 | // process.env.AWS_ACCESS_KEY_ID = process.env.SES_KEY;
17 | // process.env.AWS_SECRET_ACCESS_KEY = process.env.SES_SECRET;
18 | //
19 | // const sesClient = new SESClient({
20 | // apiVersion: '2010-12-01',
21 | // region: emailData.SES_REGION,
22 | // credentials: defaultProvider,
23 | // });
24 | //
25 | // module.exports = {
26 | // klassiSendMail: async () => {
27 | // let fileList = [];
28 | //
29 | // if (remoteService && remoteService.type === 'lambdatest') {
30 | // if (emailData.AccessibilityReport === 'Yes') {
31 | // fileList = fileList.concat(accessibilityReportList);
32 | // }
33 | // }
34 | //
35 | // const devTeam = emailData.nameList;
36 | //
37 | // const transporter = nodemailer.createTransport({
38 | // SES: { sesClient },
39 | // statement: [
40 | // {
41 | // Effect: 'Allow',
42 | // Action: 'ses:SendRawEmail',
43 | // Resource: '*',
44 | // },
45 | // ],
46 | // });
47 | //
48 | // const mailOptions = {
49 | // to: devTeam,
50 | // from: 'KLASSI-QATEST ',
51 | // subject: `${projectName} ${reportName}-${emailDateTime}`,
52 | // alternative: true,
53 | // attachments: fileList,
54 | // html: `Please find attached the automated test results for test run on - ${emailDateTime}`,
55 | // };
56 | //
57 | // await transporter.verify(async (err, success) => {
58 | // if (err) {
59 | // console.error('Server failed to Start', err.stack);
60 | // } else {
61 | // console.info('Server is ready to take our messages');
62 | // }
63 | //
64 | // if (success) {
65 | // try {
66 | // await transporter.sendMail(mailOptions, (err) => {
67 | // if (err) {
68 | // console.error(`Results Email CANNOT be sent: ${err.stack}`);
69 | // throw err;
70 | // } else {
71 | // console.info('Results Email successfully sent');
72 | // browser.pause(DELAY_200ms).then(() => {
73 | // process.exit(0);
74 | // });
75 | // }
76 | // });
77 | // } catch (err) {
78 | // console.error('There is a system error: ', err.stack);
79 | // throw err;
80 | // }
81 | // }
82 | // });
83 | // },
84 | // };
85 |
86 | const { SESClient, SendRawEmailCommand } = require('@aws-sdk/client-ses');
87 | const fs = require('fs');
88 | const path = require('path');
89 | const getRemote = require('./getRemote');
90 |
91 | const remoteService = getRemote(settings.remoteService);
92 | const emailDateTime = helpers.emailReportDateTime();
93 |
94 | // Create SES client with AWS SDK v3
95 | const sesClient = new SESClient({
96 | region: emailData.SES_REGION || 'eu-west-1',
97 | credentials: {
98 | accessKeyId: process.env.SES_KEY,
99 | secretAccessKey: process.env.SES_SECRET,
100 | },
101 | });
102 |
103 | // Simple function to clean email addresses
104 | const cleanEmail = (email) => {
105 | if (!email) return email;
106 |
107 | // Remove all whitespace and control characters first
108 | let cleaned = email.trim().replace(/[\s\u0000-\u001F\u007F-\u009F]/g, '');
109 |
110 | // Remove angle brackets and extract just the email part
111 | // Handle "Name" format
112 | const match = cleaned.match(/^(.+)<(.+@.+)>$/);
113 | if (match) {
114 | cleaned = match[2]; // Return just the email part
115 | }
116 |
117 | // Handle "" format
118 | const simpleMatch = cleaned.match(/^<(.+@.+)>$/);
119 | if (simpleMatch) {
120 | cleaned = simpleMatch[1]; // Return just the email part
121 | }
122 |
123 | // Final cleanup - remove any remaining whitespace
124 | cleaned = cleaned.trim();
125 |
126 | return cleaned;
127 | };
128 |
129 | // Function to create raw email message with proper attachment handling
130 | const createRawEmail = (from, to, subject, html, attachments = []) => {
131 | const boundary = 'boundary_' + Math.random().toString(36).substr(2, 9);
132 |
133 | let rawMessage = '';
134 | rawMessage += `From: ${from}\r\n`;
135 | rawMessage += `To: ${to}\r\n`;
136 | rawMessage += `Subject: ${subject}\r\n`;
137 | rawMessage += `MIME-Version: 1.0\r\n`;
138 | rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`;
139 |
140 | // HTML part
141 | rawMessage += `--${boundary}\r\n`;
142 | rawMessage += `Content-Type: text/html; charset=UTF-8\r\n`;
143 | rawMessage += `Content-Transfer-Encoding: 7bit\r\n\r\n`;
144 | rawMessage += `${html}\r\n\r\n`;
145 |
146 | // Attachments
147 | attachments.forEach((attachment, index) => {
148 | try {
149 | // Handle different attachment formats
150 | let content, filename, contentType;
151 |
152 | if (typeof attachment === 'string') {
153 | // If attachment is a file path
154 | if (fs.existsSync(attachment)) {
155 | content = fs.readFileSync(attachment);
156 | filename = path.basename(attachment);
157 | contentType = getContentType(filename);
158 | } else {
159 | return;
160 | }
161 | } else if (attachment && attachment.content) {
162 | // If attachment has content property
163 | content = attachment.content;
164 | filename = attachment.filename || 'attachment';
165 | contentType = attachment.contentType || 'application/octet-stream';
166 | } else if (attachment && attachment.path) {
167 | // If attachment has path property
168 | if (fs.existsSync(attachment.path)) {
169 | content = fs.readFileSync(attachment.path);
170 | filename = attachment.filename || path.basename(attachment.path);
171 | contentType = attachment.contentType || getContentType(filename);
172 | } else {
173 | return;
174 | }
175 | } else {
176 | return;
177 | }
178 |
179 | // Convert content to base64 if it's a buffer
180 | const base64Content = Buffer.isBuffer(content) ? content.toString('base64') : content;
181 |
182 | rawMessage += `--${boundary}\r\n`;
183 | rawMessage += `Content-Type: ${contentType}\r\n`;
184 | rawMessage += `Content-Transfer-Encoding: base64\r\n`;
185 | rawMessage += `Content-Disposition: attachment; filename="${filename}"\r\n\r\n`;
186 | rawMessage += `${base64Content}\r\n\r\n`;
187 |
188 | } catch (error) {
189 | console.error('Error processing attachment:', error);
190 | }
191 | });
192 |
193 | rawMessage += `--${boundary}--\r\n`;
194 |
195 | return rawMessage;
196 | };
197 |
198 | // Helper function to determine content type based on file extension
199 | const getContentType = (filename) => {
200 | const ext = path.extname(filename).toLowerCase();
201 | const contentTypes = {
202 | '.html': 'text/html',
203 | '.htm': 'text/html',
204 | '.txt': 'text/plain',
205 | '.pdf': 'application/pdf',
206 | '.png': 'image/png',
207 | '.jpg': 'image/jpeg',
208 | '.jpeg': 'image/jpeg',
209 | '.gif': 'image/gif',
210 | '.xml': 'application/xml',
211 | '.json': 'application/json',
212 | '.csv': 'text/csv'
213 | };
214 | return contentTypes[ext] || 'application/octet-stream';
215 | };
216 |
217 | // Function to find accessibility reports
218 | const findAccessibilityReports = () => {
219 | const reports = [];
220 |
221 | // Calculate today's date range (since the correct reports are generated today)
222 | const now = new Date();
223 | const today = new Date(now);
224 | today.setHours(0, 0, 0, 0);
225 | const todayEnd = new Date(today);
226 | todayEnd.setHours(23, 59, 59, 999);
227 |
228 | // Check common report directories
229 | const reportDirs = [
230 | './reports',
231 | './test-results',
232 | './results',
233 | './output',
234 | './dist',
235 | './build',
236 | './coverage',
237 | './reports/accessibility',
238 | './test-results/accessibility'
239 | ];
240 |
241 | reportDirs.forEach(dir => {
242 | if (fs.existsSync(dir)) {
243 | try {
244 | const files = fs.readdirSync(dir);
245 | files.forEach(file => {
246 | const filePath = path.join(dir, file);
247 | const stats = fs.statSync(filePath);
248 | // Only include files modified today
249 | if (
250 | stats.isFile() &&
251 | stats.mtime >= today &&
252 | stats.mtime <= todayEnd &&
253 | (
254 | file.toLowerCase().includes('accessibility') ||
255 | file.toLowerCase().includes('a11y') ||
256 | file.toLowerCase().includes('report') ||
257 | file.endsWith('.html') ||
258 | file.endsWith('.json') ||
259 | file.endsWith('.xml')
260 | )
261 | ) {
262 | reports.push(filePath);
263 | }
264 | });
265 | } catch (error) {
266 | // Silently continue if directory can't be read
267 | }
268 | }
269 | });
270 |
271 | // Also check for any HTML files in current directory
272 | try {
273 | const currentDirFiles = fs.readdirSync('.');
274 | currentDirFiles.forEach(file => {
275 | const stats = fs.statSync(file);
276 | if (
277 | stats.isFile() &&
278 | stats.mtime >= today &&
279 | stats.mtime <= todayEnd &&
280 | file.endsWith('.html') &&
281 | (
282 | file.toLowerCase().includes('accessibility') ||
283 | file.toLowerCase().includes('a11y') ||
284 | file.toLowerCase().includes('report')
285 | )
286 | ) {
287 | reports.push(file);
288 | }
289 | });
290 | } catch (error) {
291 | // Silently continue if current directory can't be read
292 | }
293 |
294 | return reports;
295 | };
296 |
297 | module.exports = {
298 | klassiSendMail: async () => {
299 | try {
300 | let fileList = [];
301 |
302 | if (remoteService && remoteService.type === 'lambdatest') {
303 | if (!emailData.AccessibilityReport || emailData.AccessibilityReport === 'Yes') {
304 | // Search for accessibility reports in the file system
305 | const foundReports = findAccessibilityReports();
306 | if (foundReports.length > 0) {
307 | fileList = fileList.concat(foundReports);
308 | }
309 |
310 | // Also add the original accessibilityReportList if it has content
311 | if (accessibilityReportList && accessibilityReportList.length > 0) {
312 | fileList = fileList.concat(accessibilityReportList);
313 | }
314 | }
315 | }
316 |
317 | // Clean the email addresses
318 | const nameListRaw = emailData.nameList;
319 | const nameListArray = Array.isArray(nameListRaw)
320 | ? nameListRaw
321 | : nameListRaw.split(',').map(e => e.trim()).filter(Boolean);
322 |
323 | const devTeam = nameListArray
324 | .map(cleanEmail)
325 | .filter(e => !!e && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(e));
326 |
327 | const recipientEmail = devTeam.join(',');
328 |
329 | if (!devTeam || devTeam.length === 0 || !recipientEmail) {
330 | console.error('No valid email addresses found, early return');
331 | return;
332 | }
333 |
334 | const senderEmail = 'KLASSI-QAAUTOTEST ';
335 | const subject = `${projectName} ${reportName}-${emailDateTime}`;
336 | const html = `Please find attached the automated test results for test run on - ${emailDateTime}`;
337 |
338 | try {
339 | // Create raw email message
340 | const rawMessage = createRawEmail(senderEmail, recipientEmail, subject, html, fileList);
341 |
342 | const params = {
343 | RawMessage: {
344 | Data: Buffer.from(rawMessage, 'utf8')
345 | }
346 | };
347 |
348 | const command = new SendRawEmailCommand(params);
349 | const result = await sesClient.send(command);
350 |
351 | console.log('Results Email successfully sent');
352 | browser.pause(DELAY_200ms).then(() => {
353 | process.exit(0);
354 | });
355 |
356 | } catch (error) {
357 | console.error('Results Email CANNOT be sent:', error.message, error);
358 | throw error;
359 | }
360 | } catch (err) {
361 | console.error('Uncaught error in oupSendMail:', err);
362 | }
363 | },
364 | };
365 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # klassijs Automated Testing Tool
2 | # Created by Larry Goddard
3 | # Javascript Node CircleCI 2.1 configuration file
4 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
5 |
6 | version: 2.1
7 |
8 | references:
9 | filter_ignore_wip_branches:
10 | filters:
11 | branches:
12 | ignore:
13 | - /[a-z]{2}_wip.*/ # wip branches with initials prefix, e.g. mw_wip
14 | - /wip\/.*/ # wip branches with wip/ prefix, e.g. wip/foo
15 | - /poc\/.*/ # poc branches with poc/ prefix, e.g. poc/bar
16 | - /automation\/.*/ # automation branches with automation/ prefix. e.g. automation/foo
17 | - /feature\/.*/ # feature branches with feature/ prefix. e.g. feature/foo
18 |
19 | filter_only_integration_branches: &filter_only_integration_branches
20 | filters:
21 | branches:
22 | only:
23 | - develop # git flow develop branch
24 | - /testfix\/.*/ # testfix branches with testfix/ prefix. e.g. testfix/foo
25 |
26 | filter_only_testable_branches: &filter_only_testable_branches
27 | filters:
28 | branches:
29 | only:
30 | - master # git flow master branch
31 | - main # git flow main branch
32 | - develop # git flow develop branch
33 | - /feature\/.*/ # git flow hotfix branches
34 | - /hotfix\/.*/ # git flow hotfix branches
35 | - /release\/.*/ # git flow release branches
36 | - /testfix\/.*/ # testfix branches with testfix/ prefix. e.g. testfix/foo
37 |
38 | filter_only_releasable_branches: &filter_only_releasable_branches
39 | filters:
40 | branches:
41 | only:
42 | - master # git flow master branch
43 | - main # git flow main branch
44 | - /hotfix\/.*/ # git flow hotfix branches
45 | - /release\/.*/ # git flow release branches
46 |
47 | jobs:
48 | #########################
49 | ### build and install ###
50 | #########################
51 | build_install: &build_install
52 | docker:
53 | - image: cimg/node:20.18.3-browsers
54 | resource_class: small
55 | working_directory: ~/klassijs
56 |
57 | steps:
58 | # Install pnpm globally
59 | - run:
60 | name: Install pnpm
61 | command: |
62 | export SHELL=/bin/bash
63 | curl -fsSL https://get.pnpm.io/install.sh | sh -
64 |
65 | # Add PNPM to the path
66 | - run:
67 | name: Set up PNPM environment
68 | command: export PATH="$HOME/.local/share/pnpm:$PATH"
69 |
70 | # Verify pnpm installation
71 | - run:
72 | name: Check pnpm version
73 | command: pnpm --version
74 |
75 | - run:
76 | name: Avoid hosts unknown for github
77 | command: mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
78 |
79 | - run:
80 | name: Remove the old klassi-js Repo
81 | command: rm -rf ~/klassijs/klassi-js
82 |
83 | - run:
84 | name: Checkout the klassi-js framework repo
85 | command: git clone -b main git@github.com:klassijs/klassi-js.git
86 |
87 | - run:
88 | name: Check out the working branch
89 | path: ~/klassijs/klassi-js
90 | command: git checkout ${CIRCLE_BRANCH}
91 |
92 | # Generate a robust dependency cache key using SHA-256
93 | - run:
94 | name: Generate dependency cache key
95 | command: |
96 | if [ -f pnpm-lock.yaml ]; then
97 | shasum -a 256 ~/klassijs/klassi-js/pnpm-lock.yaml > CACHE_KEY
98 | else
99 | echo "pnpm-lock.yaml not found, using package.json instead"
100 | shasum -a 256 ~/klassijs/klassi-js/package.json > CACHE_KEY
101 | fi
102 |
103 | # Restore cache if available
104 | - restore_cache:
105 | name: Restore pnpm cache
106 | keys:
107 | - pnpm-cache-v1-{{ checksum "CACHE_KEY" }}
108 | - pnpm-cache-v1- # Fallback cache key
109 |
110 | - run:
111 | name: Install the klassi-js framework dependencies
112 | path: ~/klassijs/klassi-js
113 | command: |
114 | git config --global url."git@github.com:".insteadOf "https://github.com/"
115 | pnpm install
116 |
117 | - run:
118 | name: Run pre-commit checks
119 | command: |
120 | cd ~/klassijs/klassi-js
121 | ./runtime/scripts/precommit-checks.sh
122 |
123 | ##########################################################################
124 | # Save pnpm cache, don't include node modules because we end up with an #
125 | # archive so large that unarchiving takes longer than the pnpm install #
126 | ##########################################################################
127 | # Save cache
128 | - save_cache:
129 | key: pnpm-cache-v1-{{ checksum "CACHE_KEY" }}
130 | paths:
131 | - ~/.pnpm-store # PNPM's global store for caching dependencies
132 |
133 | - persist_to_workspace:
134 | root: ./
135 | paths:
136 | - ./klassi-js
137 |
138 | build_and_install:
139 | <<: *build_install
140 |
141 | #####################################################
142 | ## Checkout Project Repos ##
143 | #####################################################
144 | project_code_check_out: &project_code_check_out
145 | docker:
146 | - image: cimg/node:20.18.3-browsers
147 | resource_class: small
148 |
149 | steps:
150 | - attach_workspace:
151 | at: ./
152 |
153 | - run:
154 | name: Avoid hosts unknown for github
155 | command: mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
156 |
157 | - run:
158 | name: Create projects directory
159 | command: mkdir ./projects
160 |
161 | - run:
162 | name: Checkout the Project Repos
163 | path: ./projects
164 | command: |
165 | exec < "../klassi-js/runtime/scripts/projectList.csv"
166 | read -r header
167 | while IFS=", ", read -r name branch folder; do
168 | if test "$branch"; then
169 | echo "#################################################### "
170 | echo "# Cloning the $name Project "
171 | echo "#################################################### "
172 | git clone -b $branch git@github.com:klassijs/$name.git || true
173 | else
174 | git clone -b $branch git@github.com:klassijs/$name.git || true
175 | fi
176 | done
177 |
178 | #####################################################
179 | ## Generate dependency cache key ##
180 | #####################################################
181 | - run:
182 | name: Generate dependency cache key
183 | path: ./projects
184 | command: |
185 | exec < "../klassi-js/runtime/scripts/projectList.csv"
186 | read -r header
187 | while IFS=",", read -r name branch folder; do
188 | if test "$folder"; then
189 | echo "this is the list 1:- $name $branch $folder"
190 | cd $name/test
191 | cat shasum -a 256 ~/klassijs/klassi-js/pnpm-lock.yaml > CACHE_KEY
192 | cd ../../
193 | else
194 | echo "this is the list 2:- $name $branch $folder"
195 | cat $name/shasum -a 256 ~/klassijs/klassi-js/pnpm-lock.yaml > CACHE_KEY
196 | fi
197 | done
198 |
199 | # Restore cache if available
200 | - restore_cache:
201 | keys:
202 | - pnpm-cache-v1-{{ checksum "CACHE_KEY" }}
203 | - pnpm-cache-v1- # Fallback cache key
204 |
205 | ##########################################################################
206 | # Save pnpm cache, don't include node modules because we end up with an #
207 | # archive so large that unarchiving takes longer than the pnpm install #
208 | ##########################################################################
209 | # Save cache
210 | - save_cache:
211 | key: pnpm-cache-v1-{{ checksum "CACHE_KEY" }}
212 | paths:
213 | - ~/.pnpm-store # PNPM's global store for caching dependencies
214 |
215 | #####################################################
216 | ## Install all Project dependencies ##
217 | #####################################################
218 | - run:
219 | name: Install all Project Dependencies
220 | path: ./projects
221 | command: |
222 | exec < "../klassi-js/runtime/scripts/projectList.csv"
223 | read -r header
224 | while IFS=", ", read -r name branch folder; do
225 | cd $name
226 | echo "#################################################### "
227 | echo "# Installing $name Project Dependencies...... "
228 | echo "#################################################### "
229 | if test "$folder"; then
230 | cd test
231 | git config --global url."git@github.com:".insteadOf "https://github.com/"
232 | pnpm install --network-concurrency 1 || true
233 | cd ../
234 | else
235 | git config --global url."git@github.com:".insteadOf "https://github.com/"
236 | pnpm install --network-concurrency 1 || true
237 | fi
238 | cd ../
239 | done
240 |
241 | - persist_to_workspace:
242 | root: ./
243 | paths:
244 | - ./projects
245 |
246 | project_code_checkout:
247 | <<: *project_code_check_out
248 |
249 | ###########################################
250 | # s3 Report base configuration #
251 | ###########################################
252 | s3_report_base: &s3_report_base
253 | docker:
254 | - image: cimg/node:20.18.3-browsers
255 | resource_class: small
256 | steps:
257 | - attach_workspace:
258 | at: ./
259 |
260 | ###############################################
261 | # Lambdatest setup for execution and shutdown #
262 | ###############################################
263 | - run:
264 | name: Lambdatest tunnel file downloading and upziping
265 | command: |
266 | sudo apt-get update && sudo apt-get install -y curl unzip iproute2 psmisc
267 | curl -O https://downloads.lambdatest.com/tunnel/v3/linux/64bit/LT_Linux.zip
268 | unzip LT_Linux.zip
269 | chmod +x LT
270 | rm LT_Linux.zip
271 |
272 | - run:
273 | name: Adding tunnel name to project
274 | background: true
275 | command: echo 'export TUNNEL_NAME=${STAGE}-${BROWSER}-${CIRCLE_BUILD_NUM}' >> $BASH_ENV
276 |
277 | - run:
278 | name: Starting Lambdatest Tunnel for testing
279 | background: true
280 | command: |
281 | if [ -z $LAMBDATEST_ACCESS_KEY ]; then
282 | echo "Error: The parameter lambdatest_key is empty. Please ensure the environment variable LAMBDATEST_KEY has been added."
283 | exit 1
284 | fi
285 | read LOWERPORT UPPERPORT < /proc/sys/net/ipv4/ip_local_port_range
286 | PORT=$LOWERPORT
287 | while [ $PORT -lt $UPPERPORT ]; do
288 | ss -lpn | grep -q ":$PORT " || break
289 | let PORT=PORT+1
290 | done
291 | echo "$PORT" > /tmp/port
292 | echo $TUNNEL_NAME
293 | ./LT --user $LAMBDATEST_USERNAME --key $LAMBDATEST_ACCESS_KEY --controller circleci --infoAPIPort $PORT --tunnelName $TUNNEL_NAME
294 |
295 | - run:
296 | name: Wait for LambdaTest Tunnel confirmation
297 | command: |
298 | while [ ! -f /tmp/port ]; do sleep 0.5; done
299 | PORT=$(head -1 /tmp/port)
300 | curl --silent --retry-connrefused --connect-timeout 5 --max-time 5 --retry 10 --retry-delay 2 --retry-max-time 30 http://127.0.0.1:$PORT/api/v1.0/info 2>&1 > /dev/null
301 |
302 | #########################
303 | ## s3 report Execution ##
304 | #########################
305 | - run:
306 | name: Executing the s3 Report
307 | path: ./projects
308 | command: |
309 | exitCode=0
310 | exec < ../klassi-js/runtime/scripts/projectList.csv
311 | read -r header
312 | while IFS=",", read -r name branch folder; do
313 | cd $name
314 | echo "#################################################### "
315 | echo "# The $name Project s3 Report # "
316 | echo "#################################################### "
317 | if test "$folder"; then
318 | cd $folder
319 | pnpm cilts3r chrome || exitCode=1
320 | cd ../../
321 | else
322 | pnpm cilts3r chrome || exitCode=1
323 | cd ../
324 | fi
325 | done
326 | exit $exitCode
327 |
328 | s3Report_email_run:
329 | <<: *s3_report_base
330 |
331 | ###########################################
332 | # Unit test base configuration #
333 | ###########################################
334 | unit_test_base: &unit_test_base
335 | docker:
336 | - image: cimg/node:20.18.3-browsers
337 | resource_class: small
338 | steps:
339 | - attach_workspace:
340 | at: ./
341 |
342 | - run:
343 | name: Setting up tmp Dir for split files
344 | command: mkdir ./tmp
345 |
346 | - run:
347 | name: Run unit tests
348 | path: ./klassi-js
349 | command: |
350 | pnpm test:checkin
351 |
352 | - store_test_results:
353 | path: ./klassi-js/reports
354 |
355 | unit_test:
356 | <<: *unit_test_base
357 |
358 | ###########################################
359 | # Integration test base configuration #
360 | ###########################################
361 | integration_test_base: &integration_test_base
362 | docker:
363 | - image: cimg/node:20.18.3-browsers
364 | resource_class: small
365 | steps:
366 | - attach_workspace:
367 | at: ./
368 |
369 | - run:
370 | name: Setting up tmp Dir for split files
371 | command: mkdir ./tmp
372 |
373 | - run:
374 | name: Run integration tests
375 | path: ./klassi-js
376 | command: |
377 | pnpm test:merge
378 |
379 | - store_test_results:
380 | path: ./klassi-js/reports
381 |
382 | integration_test:
383 | <<: *integration_test_base
384 |
385 | workflows:
386 | version: 2
387 | build_and_test:
388 | jobs:
389 | - build_and_install:
390 | context: klassi-framework
391 | <<: *filter_only_testable_branches
392 |
393 | - unit_test:
394 | context: klassi-framework
395 | requires:
396 | - build_and_install
397 | <<: *filter_only_testable_branches
398 |
399 | - integration_test:
400 | context: klassi-framework
401 | requires:
402 | - unit_test
403 | <<: *filter_only_releasable_branches
404 |
405 | # s3Report_run:
406 | # triggers:
407 | # - schedule:
408 | # cron: "00 04 * * *"
409 | # filters:
410 | # branches:
411 | # only:
412 | # - main
413 | #
414 | # jobs:
415 | # - build_and_install:
416 | # context: klassi-framework
417 | #
418 | # - project_code_checkout:
419 | # context: klassi-framework
420 | # requires:
421 | # - build_and_install
422 | #
423 | # - s3Report_email_run:
424 | # context: klassi-framework
425 | # requires:
426 | # - project_code_checkout
427 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi-js
3 | * Copyright © 2016 - Larry Goddard
4 |
5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | */
8 | require('dotenv').config();
9 |
10 | const fs = require('fs-extra');
11 | const merge = require('merge');
12 | const { Command } = require('commander');
13 | const requireDir = require('require-dir');
14 | const path = require('path');
15 | const loadTextFile = require('text-files-loader');
16 | const { cosmiconfigSync } = require('cosmiconfig');
17 | const {
18 | After,
19 | AfterAll,
20 | AfterStep,
21 | Status,
22 | Before,
23 | BeforeAll,
24 | BeforeStep,
25 | Given,
26 | When,
27 | Then,
28 | And,
29 | But,
30 | } = require('@cucumber/cucumber');
31 | const { runCucumber, loadConfiguration } = require('@cucumber/cucumber/api');
32 | const { promisify } = require('util');
33 | const sleep = promisify(setTimeout);
34 |
35 | const { astellen } = require('klassijs-astellen');
36 |
37 | const program = new Command();
38 |
39 | const pjson = require('./package.json');
40 | const takeReportBackup = require('./runtime/utils/takeReportBackup');
41 |
42 | async function klassiCli() {
43 | try {
44 | const { runConfiguration } = await loadConfiguration();
45 | const { success } = await runCucumber(runConfiguration);
46 | return success;
47 | } catch (error) {
48 | console.error('Error in klassiCli:', error);
49 | process.exit(1);
50 | }
51 | }
52 |
53 | (async () => {
54 | const chai = await import('chai');
55 | global.assert = chai.assert;
56 | })();
57 |
58 | global.fs = fs;
59 | global.sleep = sleep;
60 |
61 | /**
62 | * Global timeout to be used in test code
63 | * @type {number}
64 | */
65 | global.DELAY_100ms = 100;
66 | global.DELAY_200ms = 200;
67 | global.DELAY_300ms = 300;
68 | global.DELAY_500ms = 500;
69 | global.DELAY_750ms = 750;
70 | global.DELAY_1s = 1000;
71 | global.DELAY_2s = 2000;
72 | global.DELAY_3s = 3000;
73 | global.DELAY_5s = 5000;
74 | global.DELAY_7s = 7000;
75 | global.DELAY_8s = 8000;
76 | global.DELAY_10s = 10000;
77 | global.DELAY_15s = 15000;
78 | global.DELAY_20s = 20000;
79 | global.DELAY_30s = 30000;
80 | global.DELAY_40s = 40000;
81 | global.DELAY_1m = 60000;
82 | global.DELAY_2m = 120000;
83 | global.DELAY_3m = 180000;
84 |
85 | /**
86 | * All Cucumber Global variables
87 | * @constructor
88 | */
89 | global.Given = Given;
90 | global.When = When;
91 | global.Then = Then;
92 | global.And = And;
93 | global.But = But;
94 | global.After = After;
95 | global.AfterAll = AfterAll;
96 | global.AfterStep = AfterStep;
97 | global.Before = Before;
98 | global.BeforeAll = BeforeAll;
99 | global.BeforeStep = BeforeStep;
100 | global.Status = Status;
101 |
102 | global.projectRootPath = path.resolve(__dirname);
103 |
104 | function collectPaths(value, paths) {
105 | paths.push(value);
106 | return paths;
107 | }
108 |
109 | function parseRemoteArguments(argumentString) {
110 | if (!argumentString) {
111 | throw new Error('Expected an argumentString');
112 | }
113 | const argSplit = argumentString.split('/');
114 | const CONFIG = 0;
115 | const TAGS = 1;
116 | return {
117 | config: argSplit[CONFIG],
118 | tags: argSplit[TAGS],
119 | };
120 | }
121 |
122 | program
123 | .version(pjson.version)
124 | .description(pjson.description)
125 | .option('--browser ', 'name of browser to use (chrome, firefox). defaults to chrome', 'chrome')
126 | .option('--context ', 'contextual root path for project-specific features, steps, objects etc', './')
127 | .option('--disableReport', 'Disables the auto opening of the test report in the browser. defaults to true')
128 | .option('--email', 'email for sending reports to stakeholders', false)
129 | .option('--featureFiles ', 'comma-separated list of feature files to run defaults to ./features', 'features')
130 | .option('--reportName ', 'basename for report files e.g. use report for report.json'.reportName)
131 | .option('--env ', 'name of environment to run the framework / test in. default to test', 'test')
132 | .option(
133 | '--sharedObjects ',
134 | 'path to shared objects (repeatable). defaults to ./shared-objects',
135 | 'shared-objects',
136 | )
137 | .option('--pageObjects ', 'path to page objects. defaults to ./page-objects', 'page-objects')
138 | .option('--reports ', 'output path to save reports. defaults to ./reports', 'reports')
139 | .option('--headless', 'whether to run browser in headless mode. defaults to false', false)
140 | .option('--steps ', 'path to step definitions. defaults to ./step_definitions', 'step_definitions')
141 | .option(
142 | '--tags ',
143 | 'only execute the features or scenarios with tags matching the expression (repeatable)',
144 | collectPaths,
145 | [],
146 | )
147 | .option(
148 | '--exclude ',
149 | 'excludes the features or scenarios with tags matching the expression (repeatable)',
150 | collectPaths,
151 | [],
152 | )
153 | .option('--baselineImageUpdate', 'automatically update the baseline image after a failed comparison', false)
154 | .option('--remoteService ', 'which remote browser service, if any, should be used e.g. lambdatest', '')
155 | .option('--browserOpen', 'keep the browser open after each scenario. defaults to false', false)
156 | .option('--extraSettings ', 'further piped configs split with pipes', '')
157 | .option('--dlink', 'the switch for projects with their test suite, within a Test folder of the repo', false)
158 | .option(
159 | '--dryRun',
160 | 'the effect is that Cucumber will still do all the aggregation work of looking at your feature files, loading your support code etc but without actually executing the tests',
161 | false,
162 | )
163 | .option(
164 | '--s3Date',
165 | 'this switches the s3 date to allow the downloading and emailing of reports from the latest test run and not last nights run',
166 | false,
167 | )
168 | .option('--useProxy', 'This is in-case you need to use the proxy server while testing', false)
169 | .option('--skipTag ', 'provide a tag and all tests marked with it will be skipped automatically')
170 | .option('--isCI', 'This is to stop the html from being created while running in the CI', false)
171 | .option('--reportBackup', 'This to clear the "reports" folder & keep the record in back-up folder', false)
172 | .option('--reportClear', 'This to clear the "reports" folder', false);
173 |
174 | program.parse(process.argv);
175 | const options = program.opts();
176 |
177 | program.on('--help', () => {
178 | console.info('For more details please visit https://github.com/klassijs/klassi-js#readme\n');
179 | });
180 |
181 | const settings = {
182 | projectRoot: options.context,
183 | reportName: options.reportName,
184 | disableReport: options.disableReport,
185 | remoteService: options.remoteService,
186 | extraSettings: options.extraSettings,
187 | baselineImageUpdate: options.baselineImageUpdate,
188 | };
189 |
190 | global.settings = settings;
191 | global.BROWSER_NAME = options.browser;
192 | global.headless = options.headless;
193 | global.browserOpen = options.browserOpen;
194 | global.dryRun = options.dryRun;
195 | global.email = options.email;
196 | global.s3Date = options.s3Date;
197 | global.useProxy = options.useProxy;
198 | global.skipTag = options.skipTag;
199 | global.isCI = options.isCI;
200 | global.dlink = options.dlink;
201 | global.baselineImageUpdate = options.baselineImageUpdate;
202 |
203 | const getConfig = (configName) => cosmiconfigSync(configName).search().config;
204 | const { environment } = getConfig('envConfig');
205 | const { dataConfig } = getConfig('dataConfig');
206 |
207 | global.env = process.env.ENVIRONMENT || environment[options.env];
208 | global.dataconfig = dataConfig;
209 | global.s3Data = dataConfig.s3Data;
210 | global.emailData = dataConfig.emailData;
211 | global.projectName = process.env.PROJECT_NAME || dataConfig.projectName;
212 | global.reportName = process.env.REPORT_NAME || 'Automated Report';
213 | global.tagNames = dataConfig.tagNames;
214 |
215 | const helpers = require('./runtime/helpers');
216 | global.helpers = helpers;
217 |
218 | global.date = helpers.currentDate();
219 | global.dateTime = helpers.reportDateTime();
220 |
221 | if (!global.isCI) {
222 | if (options.reportBackup) {
223 | takeReportBackup.backupReport();
224 | }
225 | if (options.reportClear) {
226 | takeReportBackup.clearReport();
227 | }
228 | }
229 |
230 | if (options.remoteService && options.extraSettings) {
231 | const additionalSettings = parseRemoteArguments(options.extraSettings);
232 | global.remoteConfig = additionalSettings.config;
233 | if (additionalSettings.tags) {
234 | if (options.tags.length !== 0) {
235 | throw new Error('Cannot set two types of tags - either use --extraSettings or --tags');
236 | }
237 | options.tags = [additionalSettings.tags];
238 | }
239 | }
240 |
241 | global.browserName = global.remoteConfig || BROWSER_NAME;
242 | astellen.set('BROWSER_NAME', options.browser);
243 |
244 | function getProjectPath(objectName) {
245 | return path.resolve(settings.projectRoot + options[objectName]);
246 | }
247 |
248 | const paths = {
249 | pageObjects: getProjectPath('pageObjects'),
250 | reports: getProjectPath('reports'),
251 | featureFiles: getProjectPath('featureFiles'),
252 | sharedObjects: getProjectPath('sharedObjects'),
253 | };
254 |
255 | /** expose settings and paths for global use */
256 | global.paths = paths;
257 |
258 | const envName = env.envName.toLowerCase();
259 | const reports = `./reports/${global.browserName}/${envName}`;
260 |
261 | fs.ensureDirSync(reports, (err) => {
262 | if (err) {
263 | console.error(`The Reports Folder has NOT been created: ${err.stack}`);
264 | }
265 | });
266 | fs.ensureDirSync(reports + 'Combine', (err) => {
267 | if (err) {
268 | console.error(`The Reports Combine Folder has NOT been created: ${err.stack}`);
269 | }
270 | });
271 |
272 | const videoLib = path.resolve(__dirname, './runtime/getVideoLinks.js');
273 | if (fs.existsSync(videoLib)) {
274 | global.videoLib = require(videoLib);
275 | } else {
276 | console.error('No Video Lib');
277 | }
278 |
279 | let sharedObjects = {};
280 | const sharedObjectsPath = path.resolve(paths.sharedObjects);
281 | if (fs.existsSync(sharedObjectsPath)) {
282 | const allDirs = {};
283 | const dir = requireDir(sharedObjectsPath, { camelcase: true, recurse: true });
284 | sharedObjects = merge(allDirs, dir);
285 | global.sharedObjects = sharedObjects;
286 | }
287 |
288 | const pageObjectPath = path.resolve(paths.pageObjects);
289 | if (fs.existsSync(pageObjectPath)) {
290 | global.pageObjects = requireDir(pageObjectPath, {
291 | camelcase: true,
292 | recurse: true,
293 | });
294 | }
295 |
296 | function getTagsFromFeatureFiles() {
297 | let result = [];
298 | let featurefiles = {};
299 | loadTextFile.setup({ matchRegExp: /\.feature/ });
300 | const featureFilesList = options.featureFiles.split(',');
301 |
302 | featureFilesList.forEach((feature) => {
303 | const filePath = path.resolve(feature);
304 | try {
305 | const fileContent = loadTextFile.loadSync(filePath);
306 | featurefiles = Object.assign(featurefiles, fileContent);
307 | } catch (error) {
308 | console.error(`Error loading feature file ${filePath}:`, error);
309 | }
310 | });
311 |
312 | Object.keys(featurefiles).forEach((key) => {
313 | const content = String(featurefiles[key] || '', ' ');
314 | const tags = content.match(new RegExp('@[a-z0-9]+', 'g')) || [];
315 | result = result.concat(tags);
316 | });
317 | return result;
318 | }
319 |
320 | if (!options.tags || options.tags.length === 0) {
321 | process.exit(1);
322 | }
323 | let resultingString = '';
324 | if (options.tags.length > 0) {
325 | const tagsFound = getTagsFromFeatureFiles();
326 | const separateMultipleTags = options.tags[0].split(',');
327 | let separateExcludedTags;
328 |
329 | if (options.exclude && options.exclude.length >= 1) {
330 | separateExcludedTags = options.exclude[0].split(',');
331 | }
332 |
333 | const correctTags = [];
334 | const correctExcludedTags = [];
335 |
336 | for (const tag of separateMultipleTags) {
337 | if (tag[0] !== '@') {
338 | console.error('tags must start with a @');
339 | process.exit(1);
340 | }
341 | if (tagsFound.indexOf(tag) === -1) {
342 | console.error(`this tag ${tag} does not exist`);
343 | process.exit(0);
344 | }
345 | correctTags.push(tag);
346 | }
347 |
348 | if (correctTags.length === 0) {
349 | console.error('No valid tags found.');
350 | process.exit(1);
351 | }
352 |
353 | if (separateExcludedTags && separateExcludedTags.length >= 1) {
354 | for (const tag of separateExcludedTags) {
355 | if (tag[0] !== '@') {
356 | console.error('tags must start with a @');
357 | process.exit(1);
358 | }
359 | if (tagsFound.indexOf(tag) === -1) {
360 | console.error(`this tag ${tag} does not exist`);
361 | process.exit(0);
362 | }
363 | correctExcludedTags.push(tag);
364 | }
365 | }
366 |
367 | if (correctTags.length > 1) {
368 | resultingString = correctTags.join(' or ');
369 | if (correctExcludedTags.length > 0) {
370 | const excludedCommand = correctExcludedTags.join(' and not ');
371 | resultingString = `${resultingString} and not ${excludedCommand}`;
372 | }
373 | } else {
374 | resultingString = correctTags[0];
375 | if (correctExcludedTags.length > 0) {
376 | const excludedCommand = correctExcludedTags.join(' and not ');
377 | resultingString = `${resultingString} and not ${excludedCommand}`;
378 | }
379 | }
380 |
381 | global.resultingString = resultingString;
382 | } else {
383 | console.error('No tags provided in options.');
384 | process.exit(1);
385 | }
386 |
387 | if (options.featureFiles) {
388 | const splitFeatureFiles = options.featureFiles.split(',');
389 | global.featureFiles = splitFeatureFiles;
390 | }
391 |
392 | /** Add split to run multiple browsers from the command line
393 | * Runs the script in parallel for each browser passed via --browser.
394 | * @param {object} options - Parsed CLI options from Commander.
395 | */
396 | function handleMultipleBrowsers(options) {
397 | const { spawn } = require('child_process');
398 | if (!options.browser) return;
399 |
400 | // Prevent recursion: only run this logic if the original input had multiple browsers
401 | const originalArgv = process.argv.join(' ');
402 | if (!originalArgv.includes(',') || options.browser.includes(',')) {
403 | const browsers = options.browser.split(',').map(b => b.trim()).filter(Boolean);
404 | if (browsers.length <= 1) return;
405 |
406 | const scriptPath = process.argv[1];
407 | const args = process.argv.slice(2);
408 |
409 | const browserIndex = args.findIndex(arg => arg === '--browser');
410 | const baseArgs = args.slice(0, browserIndex);
411 | const trailingArgs = args.slice(browserIndex + 2);
412 |
413 | let completed = 0;
414 | const results = {};
415 |
416 | browsers.forEach(browser => {
417 | const child = spawn('node', [scriptPath, ...baseArgs, '--browser', browser, ...trailingArgs], {
418 | stdio: 'inherit',
419 | shell: true,
420 | });
421 |
422 | child.on('exit', code => {
423 | results[browser] = code;
424 | completed++;
425 |
426 | if (completed === browsers.length) {
427 | console.log('\n📋 Summary:');
428 | browsers.forEach(b => {
429 | if (results[b] === 0) {
430 | console.log(`✅ ${b} completed successfully`);
431 | } else {
432 | console.error(`❌ ${b} failed with exit code ${results[b]}`);
433 | }
434 | });
435 | process.exit(0);
436 | }
437 | });
438 | });
439 | // Prevent the parent from continuing
440 | process.exit(0);
441 | }
442 | }
443 | handleMultipleBrowsers(options);
444 |
445 |
446 | klassiCli().then(async (succeeded) => {
447 | let dryRun = false;
448 | if (dryRun === false) {
449 | if (!succeeded) {
450 | await cucumberCli().then(async () => {
451 | await process.exit(1);
452 | });
453 | } else {
454 | await cucumberCli().then(async () => {
455 | await sleep(DELAY_2s).then(async () => {
456 | console.info('Test run completed successfully');
457 | await process.exit(0);
458 | });
459 | });
460 | }
461 | }
462 | });
463 |
464 | async function cucumberCli() {
465 | let email = false;
466 | if (options.remoteService && options.remoteService === 'lambdatest' && resultingString !== '@s3load') {
467 | await sleep(DELAY_2s).then(async () => {
468 | await helpers.klassiReporter();
469 | });
470 | } else if (resultingString !== '@s3load') {
471 | await sleep(DELAY_2s).then(async () => {
472 | await helpers.klassiReporter();
473 | });
474 | }
475 | await sleep(DELAY_5s);
476 | if (email === true) {
477 | await sleep(DELAY_2s).then(async () => {
478 | await helpers.klassiEmail();
479 | await sleep(DELAY_3s);
480 | });
481 | }
482 | }
483 |
484 | module.exports = { getTagsFromFeatureFiles };
485 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | klassi-Js is a debuggable BDD (Behavior-Driven Development) JavaScript test automation framework, Built on webdriver.io (Next-gen browser and mobile automation test framework for Node.js) and cucumber-js and distinguished by its integration of AI for advanced debugging functionalities. This incorporation of artificial intelligence elevates the framework's capabilities, providing a streamlined and efficient approach to test automation with integrated Visual validation, accessibility and API Testing, your test can run locally or in the cloud using Lambdatest, BrowserStack or Sauce Labs
18 |
19 |
20 |
21 | ## Installation
22 |
23 | ```bash
24 | pnpm add klassi-js
25 | ```
26 |
27 | ## Usage
28 |
29 | ```bash
30 | node ./node_modules/klassi-js/index.js
31 | ```
32 |
33 | ## Options
34 |
35 | ```bash
36 | --help output usage information
37 | --version output the version number
38 | --browser name of browser to use (chrome, firefox). defaults to chrome
39 | --tags <@tagName> name of cucumber tags to run - Multiple TAGS usage (@tag1,@tag2)
40 | --exclude <@tagName> name of cucumber tags to exclude - Multiple TAGS usage(@tag3,@tag5)
41 | --steps path to step definitions. defaults to ./step-definitions
42 | --featureFiles path to feature definitions. defaults to ./features
43 | --pageObjects path to page objects. defaults to ./page-objects
44 | --sharedObjects path to shared objects - repeatable. defaults to ./shared-objects
45 | --reports output path to save reports. defaults to ./reports
46 | --disableReport disables the test report from opening after test completion
47 | --email sends email reports to stakeholders
48 | --env name of environment to run the framework/test in. default to dev
49 | --reportName name of what the report would be called i.e. 'Automated Test'
50 | --remoteService which remote driver service, if any, should be used e.g. browserstack
51 | --extraSettings further piped configs split with pipes
52 | --baselineImageUpdate automatically update the baseline image after a failed comparison. defaults to false
53 | --browserOpen this leaves the browser open after the session completes, useful when debugging test. defaults to false
54 | --dlink the switch for projects with their test suite, within a Test folder of the repo
55 | --dryRun the effect is that Cucumber will still do all the aggregation work of looking at your feature files, loading your support code etc but without actually executing the tests
56 | --useProxy this is in-case you need to use the proxy server while testing
57 | --reportBackup This is to clear the "reports" folder & keep the record in back-up folder,default value is false. While using this indicator, the name "reportBackup" needs to be added to the git ignore file
58 | --reportClear This is to clear the "reports" folder, default value is false
59 | --skipTag <@tagName> provide a tag and all tests marked with it will be skipped automatically.
60 | ```
61 | ## Options Usage
62 | ```bash
63 | --tags @get,@put || will execute the scenarios tagged with the values provided. If multiple are necessary, separate them with a comma (no blank space in between).
64 | --featureFiles features/utam.feature,features/getMethod.feature || provide specific feature files containing the scenarios to be executed. If multiple are necessary, separate them with a comma (no blank space in between).
65 | --browser firefox,chrome || will execute the tests in the browser specified. To run tests in parallel use multiple browsers, separate them with a comma (no blank space in between).
66 | ```
67 | ## Upgrading to klassi-js v6
68 | To upgrade existing projects for use with klassi-js v6, please follow these few steps [HERE](runtime/docs/klassi-js-v6-Upgrade.md)
69 |
70 | ## Directory Structure
71 | You can use the framework without any command line arguments if your application uses the following folder structure, to help with the built in functionality usage, we have added a .envConfigrc.js file at the base of the project which will contain all your env configs . You can check out the working [TEMPLATE HERE](https://github.com/klassijs/klassijs-example-project)
72 |
73 | ```bash
74 | .
75 | └── features
76 | └── search.feature
77 | └── page-objects
78 | └── search.js
79 | └── shared-objects
80 | └── searchData.js
81 | └── step_definitions
82 | └── search-steps.js
83 | .envConfigrc.js # this file will contain all your environment variables (i.e. dev, test, prod etc.,)
84 | .dataConfigrc.js # this file will contain all your project variables #projectName, emailAddresses
85 | .env # this file contains all config username and passcode and should be listed in the gitignore file
86 | cucumber.js # the cucumber configuration file
87 | ```
88 |
89 | ## Step definitions
90 | The following variables are available within the ```Given()```, ```When()``` and ```Then()``` functions:
91 |
92 | | Variable | Description |
93 | | :--- | :--- |
94 | | `browser` | an instance of [webdriverio](https://webdriver.io/docs/setuptypes.html) (_the browser_) |
95 | | `pageObjects` | collection of **page** objects loaded from disk and keyed by filename |
96 | | `sharedObjects` | collection of **shared** objects loaded from disk and keyed by filename |
97 | | `helpers` | a collection of [helper methods](runtime/helpers.js) _things webdriver.io does not provide but really should!_ |
98 |
99 |
100 | ## Klassi-js JS modules
101 |
102 | To streamline test script development and ensure consistency across projects, the following JavaScript libraries are being exported from klassi-js. This approach allows these libraries to be utilized at the project level without the need for duplicate installations, thereby reducing redundancy and potential conflicts.
103 |
104 | - Exported libraries:
105 |
106 | | JS Library | Description |
107 | | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------- |
108 | | `pactum` | A REST API testing tool for automating end-to-end, integration, contract, and component tests |
109 | | `webdriverio` | A next-gen browser and mobile automation test framework for Node.js |
110 | | `fs-extra` | Provides extra file system methods and promise support, enhancing the native fs module |
111 | | `dotenv` | Loads environment variables from a .env file into process.env to manage configuration separately from code |
112 | | `Husky` | A tool for managing Git hooks to automate tasks like linting, testing, and code formatting before commits or pushes |
113 | | `S3Client` | Part of the AWS SDK for JavaScript, it allows interaction with Amazon S3 for operations like uploading, downloading, and managing objects |
114 |
115 | ```js
116 | // usage klassi-js module at project level i.e.
117 | const pactumJs = require('klassi-js/klassiModule').pactum;
118 |
119 | require('klassi-js/klassiModule').dotenv.config();
120 |
121 | const fs = require('klassi-js/klassiModule').fs-extra;
122 |
123 | ```
124 |
125 |
126 | ## Helpers
127 | klassi-js contains a few helper methods to help along the way, these methods are:
128 |
129 |
130 | | Function | Description |
131 | | :------------------------------------------------------ | :---------------------------------------------------------------------------------- |
132 | | await helpers.loadPage('url', timeout) | Loads the required page |
133 | | await helpers.writeToTxtFile(filepath, output) | Writes content to a text file |
134 | | await helpers.readFromFile(filepath) | Reads content from a text file |
135 | | await helpers.currentDate() | Applies the current date to files |
136 | | await helpers.getCurrentDateTime() | Get current date and time |
137 | | await helpers.clickHiddenElement(selector, textToMatch) | Clicks an element that is not visible |
138 | | await helpers.getRandomIntegerExcludeFirst(range) | Get a random integer from a given range |
139 | | await helpers.getLink(selector) | Get the href link from an element |
140 | | await helpers.waitAndClick(selector) | Wait until and element is visible and click it |
141 | | await helpers.waitAndSetValue(selector, value) | Wait until element to be in focus and set the value |
142 | | await helpers.getElementFromFrame(frameName, selector) | Get element from frame or frameset |
143 | | await helpers.readFromJson() | Read from a json file |
144 | | await helpers.writeToJson() | Write data to a json file |
145 | | await helpers.mergeJson() | Merge json files |
146 | | await helpers.reportDateTime() | Reporting the current date and time |
147 | | await helpers.apiCall() | API call for GET, PUT, POST and DELETE functionality using PactumJS for API testing |
148 | | await helpers.getLink() | Get the href link from an element |
149 | | await helpers.getElementFromFrame() | Get element from frame or frameset |
150 | | await helpers.generateRandomInteger() | Generate random integer from a given range |
151 | | await helpers.randomNumberGenerator() | Generates a random 13 digit number |
152 | | await helpers.reformatDateString() | Reformats date string into string |
153 | | await helpers.sortByDate() | Sorts results by date |
154 | | await helpers.filterItem() | Filters an item from a list of items |
155 | | await helpers.filterItemAndClick() | Filters an item from a list of items and clicks on it |
156 | | await helpers.fileUpload() | Uploads a file from local system or project folder |
157 |
158 |
159 | ## Browser usage
160 | By default, the test run using Google Chrome/devtools protocol, to run tests using another browser locally supply the browser name along with the `--browser` switch
161 |
162 | | Browser | Example |
163 | | :--- | :--- |
164 | | Chrome | `--browser chrome` |
165 | | Firefox | `--browser firefox` |
166 |
167 | To run tests in parallel, supply multiple browser names separated by a comma, for example: `--browser chrome,firefox`
168 |
169 | ## Remote Browser usage
170 | To run tests in a remote browser, you can use the `--remoteService` switch to specify the service you want to use. Currently, the supported services are: LambdaTest, BrowserStack, SauceLabs, and Selenium Standalone.
171 |
172 | Selenium Standalone installation
173 | ```bash
174 | npm install -g selenium-standalone@latest
175 | selenium-standalone install
176 | ```
177 |
178 | ## Soft Assert with [klassijs-soft-assert](https://github.com/klassijs/klassijs-soft-assert#readme)
179 | The Assertion Tool is designed to enhance your testing by allowing tests to continue running when assertions fail. Instead of halting the test on failure, the tool collects all failed assertions and compiles them into a comprehensive report at the end of the test run.
180 |
181 | Usage of the Assertion Tool is straightforward. You can use it with any assertion library, such as Chai or expect-webdriverio, by passing the actual value, the assertion method, the expected value, and an optional message.
182 |
183 | ```js
184 | // usage within page-object file:
185 | const { softAssert } = require('klassijs-soft-assert');
186 |
187 | // Any Chai assert method
188 | await softAssert(actual, 'deepEqual', expected, 'Deep equality check');
189 | await softAssert(actual, 'isArray', undefined, 'Should be an array');
190 | await softAssert(actual, 'include', expected, 'Should include value');
191 |
192 | // Any expect-webdriverio method
193 | await softAssert(element, 'toBeDisplayed', undefined, 'Element should be displayed');
194 | await softAssert(element, 'toHaveAttribute', 'class', 'Element should have class');
195 | await softAssert(element, 'toHaveText', expected, 'Element should have text');
196 |
197 | // Chained expect-webdriverio methods
198 | await softAssert(element, 'not.toBeEnabled', undefined, 'Element should not be enabled');
199 | await softAssert(element, 'not.toBeSelected', undefined, 'Element should not be selected');
200 | ```
201 |
202 | ## Visual Validation with [klassijs-visual-validation](https://github.com/klassijs/klassijs-visual-validation#readme)
203 |
204 | Visual regression testing, the ability to compare a whole page screenshots or of specific parts of the application / page under test.
205 | If there is dynamic content (i.e. a clock), hide this element by passing the selector (or an array of selectors) to the takeImage function.
206 | ```js
207 | // usage within page-object file:
208 | // Use the `takeImage` method to take a screenshot and automatically compare it with the baseline:
209 | const {takeImage, compareImage} = require('klassijs-visual-validation');
210 | await takeImage('screenshot.png');
211 | ```
212 |
213 | **Advanced Usage Options**:
214 | ```javascript
215 | // Take screenshot of a specific element and compare
216 | await takeImage('button.png', '.submit-button');
217 |
218 | // Hide elements during screenshot and compare
219 | await takeImage('clean-page.png', null, '.header, .footer');
220 |
221 | // Take screenshot without comparison
222 | await takeImage('screenshot.png', null, '', false);
223 |
224 | // Use custom tolerance for comparison
225 | await takeImage('screenshot.png', null, '', true, 0.1);
226 | ```
227 |
228 | ## API Testing with [PactumJS](https://github.com/pactumjs/pactum#readme)
229 | Getting data from a JSON REST API
230 | ```js
231 | apiCall: async (url, method, auth, body, status) => {
232 | let resp;
233 | const options = {
234 | url,
235 | method,
236 | headers: {
237 | Authorization: `Bearer ${auth}`,
238 | 'content-Type': 'application/json',
239 | },
240 | body,
241 | };
242 |
243 | if (method === 'GET') {
244 | resp = await helpers.apiCall(url, 'GET', auth);
245 | return resp.statusCode;
246 | }
247 | if (method === 'POST') {
248 | resp = await helpers.apiCall(url, 'POST', auth, body, status);
249 | return resp;
250 | }
251 | }
252 | ```
253 | ## Accessibility Testing with [klassijs-a11y-validator](https://github.com/klassijs/klassijs-a11y-validator)
254 | Automated accessibility testing feature has been introduced using the Axe-Core OpenSource library.
255 |
256 | ### Sample code
257 | All the accessibility fuctions can be accessed through the global variable ``` accessibilityLib ```.
258 | | function | Description |
259 | |----------------------------|-----------------------------------------------------------------|
260 | | ``` accessibilityLib.getAccessibilityReport('PageName')```| generates the accessibility report with the given page name |
261 | | ``` accessibilityLib.getAccessibilityError()``` | returns the total number of error count for a particular page. |
262 | | ``` accessibilityLib.getAccessibilityTotalError() ``` | returns the total number of error count for all the pages in a particilar execution |
263 |
264 | ```js
265 | // usage within page-object file:
266 | const { a11yValidator } = require("klassijs-a11y-validator");
267 |
268 | When("I run the accesibility analysis for {string}", async function (PageName) {
269 | // After navigating to a particular page, just call the function to generate the accessibility report and the total error count for the page
270 | await a11yValidator(`SearchPage1-${PageName}`);
271 | });
272 | ```
273 | ## Accessibility Report
274 |
275 | HTML and JSON reports will be automatically generated and stored in the default `./reports/accessibility` folder.This location can be changed by providing a new path using the `--reports` command line switch:
276 |
277 | 
278 |
279 |
280 | ## Test Execution Reports with [klassijs-cucumber-html-reporter](https://github.com/klassijs/klassijs-cucumber-html-reporter)
281 |
282 | HTML and JSON reports will be automatically generated and stored in the default `./reports` folder. This location can be changed by providing a new path using the `--reports` command line switch:
283 |
284 | 
285 |
286 |
287 | ## Event handlers
288 |
289 | You can register event handlers for the following events within the cucumber lifecycle.
290 |
291 | const {After, Before, AfterAll, BeforeAll, BeforeStep, AfterStep} = require('@cucumber/cucumber');
292 |
293 | | Event | Example |
294 | |----------------|-------------------------------------------------------------|
295 | | Before | ```Before(function() { // This hook will be executed before all scenarios}) ``` |
296 | | After | ```After(function() {// This hook will be executed after all scenarios});``` |
297 | | BeforeAll | ```BeforeAll(function() {// perform some shared setup});``` |
298 | | AfterAll | ```AfterAll(function() {// perform some shared teardown});``` |
299 | | BeforeStep | ```BeforeStep(function() {// This hook will be executed before all steps in a scenario with tagname;``` |
300 | | AfterStep | ```AfterStep(function() {// This hook will be executed after all steps, and take a screenshot on step failure;``` |
301 |
302 | ## How to debug
303 |
304 | Most webdriverio methods return a [JavaScript Promise](https://spring.io/understanding/javascript-promises "view JavaScript promise introduction") that is resolved when the method completes. The easiest way to step in with a debugger is to add a ```.then``` method to the function and place a ```debugger``` statement within it, for example:
305 |
306 | ```js
307 | When(/^I search DuckDuckGo for "([^"]*)"$/, function (searchQuery, done) {
308 | elem = browser.$('#search_form_input_homepage').then(function(input) {
309 | expect(input).to.exist;
310 | debugger; // <<- your IDE should step in at this point, with the browser open
311 | return input;
312 | })
313 | done(); // <<- let cucumber know you're done
314 | });
315 | ```
316 |
317 | ## Commit conventions
318 |
319 | To enforce best practices in using Git for version control, this project includes a **Husky** configuration. Note that breaking the given rules will block the commit of the code.
320 |
321 | Bear in mind that the `/.circleci/config.yml` file **in each project using klassi-js as a dependency** needs to be modified to change from `pnpm install` to `pnpm install --network-concurrency 1`. This is to avoid race conditions in multiple calls to the registry during the installation process.
322 |
323 | ### Commits
324 | After committing the staged code, the Husky scripts will enforce the implementation of the [**Conventional Commits specification**](https://www.conventionalcommits.org/en/v1.0.0/#summary).
325 |
326 | To summarize them, all commits should follow the following schema:
327 |
328 | ```
329 | git commit -m ': '
330 | git commit -m 'chore(): updating config code'
331 | ```
332 |
333 | Where **type** is one of the following:
334 |
335 | - **fix**: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
336 | - **feat**: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
337 | - **BREAKING CHANGE**: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
338 | - Types other than **fix:** and **feat:** are allowed, for example @commitlint/Tconfig-conventional (based on the Angular convention) recommends **build:, chore:, ci:, docs:, style:, refactor:, perf:, test:**, and others.
339 | footers other than **BREAKING CHANGE:** may be provided and follow a convention similar to git trailer format.
340 |
341 | Please keep in mind that the **subject** must be written in lowercase.
342 |
343 | ### Branch naming
344 |
345 | The same script will also verify the naming convention. Please remember that we only allow for two possible branch prefixes:
346 |
347 | - **testfix/**
348 | - **automation/**
349 | - **feature/**
350 |
351 |
352 | ## Bugs
353 |
354 | Please raise bugs via the [klassijs issue tracker](https://github.com/klassijs/klassi-js/issues), please provide enough information for bug reproduction.
355 |
356 | ## Contributing
357 |
358 | Anyone can contribute to this project, PRs are welcome. In lieu of a formal styleguide, please take care to maintain the existing coding style.
359 |
360 | ## Credits
361 |
362 | John Doherty
363 |
364 |
365 | ## License
366 |
367 | Licenced under [MIT License](LICENSE) © 2016 [Larry Goddard](https://www.linkedin.com/in/larryg)
368 |
--------------------------------------------------------------------------------
/runtime/helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * klassi Automated Testing Tool
3 | * Created by Larry Goddard
4 | */
5 | const fs = require('fs-extra');
6 | const pactumJs = require('pactum');
7 | const { assertExpect } = require('klassijs-soft-assert');
8 | const loadConfig = require('./configLoader');
9 | const { EOL, os } = require('os');
10 |
11 | let elem;
12 | let getMethod;
13 | let resp;
14 | let modID;
15 |
16 | module.exports = {
17 | /**
18 | * returns a promise that is called when the url has loaded and the body element is present
19 | * @param {string} url to load
20 | * @param seconds
21 | * @returns {Promise}
22 | * @example
23 | * helpers.loadPage('http://www.duckduckgo.com', 5);
24 | */
25 | loadPage: async (url, seconds) => {
26 | /**
27 | * Wait function - measured in seconds for pauses during tests to give time for processes such as
28 | * a page loading or the user to see what the test is doing
29 | * @param seconds
30 | * @type {number}
31 | */
32 | const timeout = seconds || global.timeout;
33 | /**
34 | * load the url and wait for it to complete
35 | */
36 | await browser.url(url, async () => {
37 | /**
38 | * now wait for the body element to be present
39 | */
40 | await browser.waitUntil(async () => await browser.execute(() => document.readyState === 'complete'), {
41 | timeoutMsg: `The web page is still not loaded after ${timeout} seconds`,
42 | });
43 | });
44 | /**
45 | * grab the userAgent details from the loaded url
46 | */
47 | cucumberThis.attach(`loaded url: ${url}`);
48 | },
49 |
50 | /**
51 | * writeTextFile write data to file on hard drive
52 | * @param filepath
53 | * @param output
54 | */
55 | writeToTxtFile: async (filepath, output) => {
56 | try {
57 | await fs.truncate(filepath, 0);
58 | await fs.writeFileSync(filepath, output);
59 | } catch (err) {
60 | console.error(`Error in writing file ${err.message}`);
61 | throw err;
62 | }
63 | },
64 |
65 | /**
66 | * append / add data to file on hard drive
67 | * @param filepath
68 | * @param output
69 | * @returns {Promise}
70 | */
71 | appendToTxtFile: async (filepath, output) => {
72 | try {
73 | await fs.openSync(filepath, 'a');
74 | await fs.appendFileSync(filepath, output + '\r\n');
75 | await fs.appendFileSync(filepath, EOL);
76 | } catch (err) {
77 | console.error(`Error in writing file ${err.message}`);
78 | throw err;
79 | }
80 | },
81 |
82 | /**
83 | * This is to read the content of a text file
84 | * @param filepath
85 | * @returns {Promise}
86 | */
87 | readFromFile: (filepath) =>
88 | new Promise((resolve) => {
89 | fs.readFile(filepath, 'utf-8', (err, data) => {
90 | data = data.toString();
91 | resolve(data);
92 | // console.log('Success - the file content ', data);
93 | });
94 | }),
95 |
96 | /**
97 | * This is to read the content of a Json file
98 | * @param filename
99 | * @returns {Promise}
100 | */
101 | readFromJson: async (filename) => {
102 | const fileContent = await fs.readJson(filename);
103 | // console.log('Success - the file content ', fileContent);
104 | return fileContent;
105 | },
106 |
107 | /**
108 | * This is to write values into a JSON file
109 | * @param filePath
110 | * @param fileContent
111 | * @returns {Promise}
112 | */
113 | writeToJson: async (filePath, fileContent) => {
114 | try {
115 | await fs.writeFile(filePath, JSON.stringify(fileContent, null, 4));
116 | // console.log('Success - the content: ', fileContent);
117 | } catch (err) {
118 | console.error('This Happened: ', err);
119 | }
120 | },
121 |
122 | /**
123 | * This is to merge content of json files
124 | * @param filePath
125 | * @param file
126 | * @returns {Promise}
127 | */
128 | mergeJson: async (filePath, file) => {
129 | const fileA = loadConfig(filePath);
130 | return Object.assign(fileA, file);
131 | },
132 |
133 | /**
134 | * Get the current date dd-mm-yyyy
135 | * @returns {string|*}
136 | */
137 | currentDate() {
138 | const today = new Date();
139 | let dd = today.getDate();
140 | let mm = today.getMonth() + 1; // January is 0!
141 | const yyyy = today.getFullYear();
142 |
143 | if (dd < 10) {
144 | dd = `0${dd}`;
145 | }
146 | if (mm < 10) {
147 | mm = `0${mm}`;
148 | }
149 | return `${dd}-${mm}-${yyyy}`;
150 | },
151 |
152 | /**
153 | * Get the current date in yyyy-mm-dd format for the s3 bucket folder
154 | * @returns {string|*}
155 | */
156 | s3BucketCurrentDate() {
157 | const today = new Date();
158 | let dd = today.getDate();
159 | let mm = today.getMonth() + 1; // January is 0!
160 | const yyyy = today.getFullYear();
161 |
162 | if (dd < 10) {
163 | dd = `0${dd}`;
164 | }
165 | if (mm < 10) {
166 | mm = `0${mm}`;
167 | }
168 | return `${yyyy}-${mm}-${dd}`;
169 | },
170 |
171 | reportDateTime() {
172 | const today = new Date();
173 | let dd = today.getDate();
174 | let mm = today.getMonth() + 1; // January is 0!
175 | const yyyy = today.getFullYear();
176 | let hours = today.getHours();
177 | let minutes = today.getMinutes();
178 | let seconds = today.getSeconds();
179 | let milliseconds = today.getMilliseconds();
180 |
181 | if (dd < 10) {
182 | dd = `0${dd}`;
183 | }
184 | if (mm < 10) {
185 | mm = `0${mm}`;
186 | }
187 | if (hours < 10) {
188 | hours = `0${hours}`;
189 | }
190 | if (minutes < 10) {
191 | minutes = `0${minutes}`;
192 | }
193 | if (seconds < 10) {
194 | seconds = `0${seconds}`;
195 | }
196 | if (milliseconds < 10) {
197 | milliseconds = `0${milliseconds}`;
198 | }
199 | return `${dd}-${mm}-${yyyy}-${hours}${minutes}${seconds}${milliseconds}`;
200 | },
201 |
202 | emailReportDateTime() {
203 | const $today = new Date();
204 | const $yesterday = $today;
205 | $yesterday.setDate($today.getDate() - 1);
206 | let dd = $yesterday.getDate();
207 | let mm = $yesterday.getMonth() + 1; // January is 0!
208 | const yyyy = $yesterday.getFullYear();
209 | let hours = $yesterday.getHours();
210 | let minutes = $yesterday.getMinutes();
211 | let seconds = $yesterday.getSeconds();
212 |
213 | if (dd < 10) {
214 | dd = `0${dd}`;
215 | }
216 | if (mm < 10) {
217 | mm = `0${mm}`;
218 | }
219 | if (hours < 10) {
220 | hours = `0${hours}`;
221 | }
222 | if (minutes < 10) {
223 | minutes = `0${minutes}`;
224 | }
225 | if (seconds < 10) {
226 | seconds = `0${seconds}`;
227 | }
228 | return `${dd}-${mm}-${yyyy}-${hours}${minutes}${seconds}`;
229 | },
230 | /**
231 | * Get current date and time dd-mm-yyyy 00:00:00
232 | */
233 | getCurrentDateTime() {
234 | const today = new Date();
235 | let dd = today.getDate();
236 | let mm = today.getMonth() + 1; // January is 0!
237 | const yyyy = today.getFullYear();
238 | let hours = today.getHours();
239 | let minutes = today.getMinutes();
240 | let seconds = today.getSeconds();
241 |
242 | if (dd < 10) {
243 | dd = `0${dd}`;
244 | }
245 | if (mm < 10) {
246 | mm = `0${mm}`;
247 | }
248 | if (hours < 10) {
249 | hours = `0${hours}`;
250 | }
251 | if (minutes < 10) {
252 | minutes = `0${minutes}`;
253 | }
254 | if (seconds < 10) {
255 | seconds = `0${seconds}`;
256 | }
257 | return `${dd}-${mm}-${yyyy}-${hours}:${minutes}:${seconds}`;
258 | },
259 |
260 | getEndDateTime() {
261 | return this.getCurrentDateTime();
262 | },
263 |
264 | getStartDateTime() {
265 | return this.getCurrentDateTime();
266 | },
267 |
268 | klassiReporter() {
269 | try {
270 | return require('./reporter/reporter').reporter();
271 | } catch (err) {
272 | console.error(`This is the Reporting System error: ${err.stack}`);
273 | throw err;
274 | }
275 | },
276 |
277 | /**
278 | * ========== EMAIL FUNCTIONALITY ==========
279 | * Sends an Email to the concerned users with the log and the test report
280 | */
281 | klassiEmail() {
282 | try {
283 | return require('./mailer').klassiSendMail();
284 | } catch (err) {
285 | console.error(`This is the Email System error: ${err.stack}`);
286 | throw err;
287 | }
288 | },
289 |
290 | /**
291 | * API call for GET, PUT, POST and DELETE functionality using PactumJS for API testing
292 | * @param url
293 | * @param method
294 | * @param auth
295 | * @param body
296 | * @param form
297 | * @param expectedStatusCode
298 | * @returns {Promise<*>}
299 | */
300 | apiCall: async (url, method, auth = null, form = null, body = null, expectedStatusCode = null) => {
301 | const options = {
302 | url,
303 | method,
304 | auth,
305 | headers: {
306 | Authorization: `${auth}`,
307 | },
308 | form,
309 | body,
310 | expectedStatusCode,
311 | };
312 | if (method === 'GET') {
313 | resp = await pactumJs
314 | .spec()
315 | .get(options.url)
316 | .withHeaders(options.headers)
317 | .withRequestTimeout(DELAY_15s)
318 | .expectStatus(expectedStatusCode)
319 | .toss();
320 | getMethod = resp;
321 | }
322 |
323 | if (method === 'PUT') {
324 | resp = await pactumJs
325 | .spec()
326 | .put(options.url)
327 | .withHeaders(options.headers)
328 | .withBody(options.body)
329 | .withRequestTimeout(DELAY_10s)
330 | .expectStatus(expectedStatusCode);
331 | getMethod = resp;
332 | }
333 |
334 | if (method === 'POST') {
335 | resp = await pactumJs
336 | .spec()
337 | .post(options.url)
338 | .withHeaders(options.headers)
339 | .withBody(options.body)
340 | .withForm(options.form)
341 | .withRequestTimeout(DELAY_10s)
342 | .expectStatus(expectedStatusCode);
343 | getMethod = resp;
344 | }
345 |
346 | if (method === 'DELETE') {
347 | resp = await pactumJs
348 | .spec()
349 | .post(options.url)
350 | .withHeaders(options.headers)
351 | .withBody(options.body)
352 | .withRequestTimeout(DELAY_10s)
353 | .expectStatus(expectedStatusCode);
354 | }
355 | },
356 |
357 | /**
358 | * this stores the content of the APIs GET call
359 | * @returns {*}
360 | */
361 | getContent() {
362 | return getMethod;
363 | },
364 |
365 | /**
366 | * getting the video link from lambdatest
367 | * @returns {Promise}
368 | */
369 | ltVideo: async () => {
370 | const page = require('./getVideoLinks');
371 | await page.getVideoList();
372 | },
373 |
374 | /**
375 | * Get the href link from an element
376 | * @param selector
377 | * @returns {String|String[]|*|string}
378 | */
379 | getLink: async (selector) => {
380 | elem = await browser.$(selector);
381 | await elem.getAttribute('href');
382 | },
383 |
384 | waitAndClick: async (selector) => {
385 | try {
386 | elem = await browser.$(selector);
387 | await elem.isExisting();
388 | await elem.click();
389 | await browser.pause(DELAY_500ms);
390 | } catch (err) {
391 | console.error(err.message);
392 | throw err;
393 | }
394 | },
395 |
396 | waitAndSetValue: async (selector, value) => {
397 | try {
398 | elem = await browser.$(selector);
399 | await elem.isExisting();
400 | await browser.pause(DELAY_500ms);
401 | await elem.addValue(value);
402 | } catch (err) {
403 | console.error(err.message);
404 | throw err;
405 | }
406 | },
407 |
408 | /**
409 | * function to get element from frame or frameset
410 | * @param frameName
411 | * @param selector
412 | * @returns {Promise.}
413 | */
414 | getElementFromFrame: async (frameName, selector) => {
415 | const frame = await browser.$(frameName);
416 | await browser.switchToFrame(frame.value);
417 | await browser.$(selector).getHTML();
418 | return browser;
419 | },
420 |
421 | /**
422 | * @param expected
423 | */
424 | assertUrl: async (expected) => {
425 | const actual = await browser.getUrl();
426 | // assert.equal(actual, expected);
427 | await helpers.expectAdv('equal', actual, expected);
428 | },
429 |
430 | /**
431 | * Generate random integer from a given range
432 | */
433 | generateRandomInteger(range) {
434 | return Math.floor(Math.random() * Math.floor(range));
435 | },
436 |
437 | /**
438 | * This method is useful for dropdown boxes as some of them have default 'Please select' option on index 0
439 | * @param range
440 | * @returns randomNumber excluding index 0
441 | */
442 | getRandomIntegerExcludeFirst(range) {
443 | let randomNumber = this.generateRandomInteger(range);
444 | if (randomNumber <= 1) {
445 | randomNumber += 2;
446 | }
447 | return randomNumber;
448 | },
449 |
450 | /**
451 | * clicks an element (or multiple if present) that is not visible,
452 | * useful in situations where a menu needs a hover before a child link appears
453 | * @param {string} selector used to locate the elements
454 | * @param {string} text to match inner content (if present)
455 | * @example
456 | * helpers.clickHiddenElement('nav[role='navigation'] ul li a','School Shoes');
457 | * @deprecated
458 | */
459 | clickHiddenElement(selector, textToMatch) {
460 | // TODO: Find a better way to do this
461 | /**
462 | * method to execute within the DOM to find elements containing text
463 | */
464 | function clickElementInDom(query, content) {
465 | /**
466 | * get the list of elements to inspect
467 | */
468 | const elements = document.querySelectorAll(query);
469 | /**
470 | * workout which property to use to get inner text
471 | */
472 | const txtProp = 'textContent' in document ? 'textContent' : 'innerText';
473 |
474 | for (let i = 0, l = elements.length; i < l; i++) {
475 | /**
476 | * If we have content, only click items matching the content
477 | */
478 | if (content) {
479 | if (elements[i][txtProp] === content) {
480 | elements[i].click();
481 | }
482 | } else {
483 | /**
484 | * otherwise click all
485 | */
486 | elements[i].click();
487 | }
488 | }
489 | }
490 |
491 | /**
492 | * grab the matching elements
493 | */
494 | return browser.$$(selector, clickElementInDom, textToMatch.toLowerCase().trim);
495 | },
496 |
497 | /**
498 | * this adds extensions to Chrome Only
499 | * @param extName
500 | * @returns {Promise<*>}
501 | */
502 | chromeExtension: async (extName) => {
503 | await browser.pause();
504 | await helpers.loadPage(`https://chrome.google.com/webstore/search/${extName}`);
505 | const script = await browser.execute(() => window.document.URL.indexOf('consent.google.com') !== -1);
506 | if (script === true) {
507 | elem = await browser.$$('[jsname="V67aGc"]:nth-child(3)');
508 | await elem[1].isExisting();
509 | await elem[1].scrollIntoView();
510 | const elem1 = await elem[1].getText();
511 | if (elem1 === 'I agree') {
512 | await elem[1].click();
513 | await browser.pause(DELAY_300ms);
514 | }
515 | }
516 | elem = await browser.$('[role="row"] > div:nth-child(1)');
517 | await elem.click();
518 | await browser.pause(DELAY_200ms);
519 | const str = await browser.getUrl();
520 | const str2 = await str.split('/');
521 | modID = str2[6];
522 | return modID;
523 | },
524 |
525 | /**
526 | * This is the function for installing modeHeader
527 | * @param extName
528 | * @param username
529 | * @param password
530 | * @returns {Promise}
531 | */
532 | modHeader: async (extName, username, password) => {
533 | await helpers.chromeExtension(extName);
534 | console.log('modID = ', modID);
535 |
536 | await browser.pause(3000);
537 | elem = await browser.$(
538 | '[class="e-f-o"] > div:nth-child(2) > [class="dd-Va g-c-wb g-eg-ua-Uc-c-za g-c-Oc-td-jb-oa g-c"]',
539 | );
540 | await elem.isExisting();
541 | await elem.click();
542 |
543 | await browser.pause(2000);
544 | elem = await browser.$('.//a[@href="#Add extension"]');
545 | await elem.isExisting();
546 | await elem.click();
547 | await helpers.loadPage(`chrome-extension://${modID}/popup.html`);
548 |
549 | await browser.pause(5000);
550 | await helpers.waitAndSetValue('(//input[@class="mdc-text-field__input "])[1]', username);
551 | await helpers.waitAndSetValue('(//input[@class="mdc-text-field__input "])[2]', password);
552 | await helpers.waitAndClick('//button[@title="Lock to tab"]');
553 | },
554 |
555 | installMobileApp: async (appName, appPath) => {
556 | if (env.envName === 'android' || env.envName === 'ios') {
557 | if (!(await browser.isAppInstalled(appName))) {
558 | console.log('Installing application...');
559 | await browser.installApp(appPath);
560 | // assert.isTrue(await browser.isAppInstalled(appName), 'The app was not installed correctly.');
561 | await assertExpect(
562 | await browser.isAppInstalled(appName),
563 | 'isTrue',
564 | null,
565 | 'The app was not installed correctly.',
566 | );
567 | } else {
568 | console.log(`The app ${appName} was already installed on the device, skipping installation...`);
569 | await browser.terminateApp(appName);
570 | }
571 | }
572 | },
573 |
574 | uninstallMobileApp: async (appName) => {
575 | if (env.envName === 'android' || env.envName === 'ios') {
576 | if (await browser.isAppInstalled(appName)) {
577 | console.log(`Uninstalling application ${appName}...`);
578 | await browser.removeApp(appName);
579 | await assertExpect(
580 | await browser.isAppInstalled(appName),
581 | 'isNotTrue',
582 | null,
583 | 'The app was not uninstalled correctly.',
584 | );
585 | // assert.isNotTrue(await browser.isAppInstalled(appName), 'The app was not uninstalled correctly.');
586 | } else {
587 | console.log(`The app ${appName} was already uninstalled fron the device, skipping...`);
588 | }
589 | }
590 | },
591 |
592 | /**
593 | * drag the page into view
594 | */
595 | pageView: async (selector) => {
596 | const elem = await browser.$(selector);
597 | await elem.scrollIntoView();
598 | await browser.pause(DELAY_200ms);
599 | return this;
600 | },
601 |
602 | /**
603 | * Generates a random 13 digit number
604 | * @param length
605 | * @returns {number}
606 | */
607 | randomNumberGenerator(length = 13) {
608 | const baseNumber = 10 ** (length - 1);
609 | let number = Math.floor(Math.random() * baseNumber);
610 | /**
611 | * Check if number have 0 as first digit
612 | */
613 | if (number < baseNumber) {
614 | number += baseNumber;
615 | }
616 | console.log(`this is the number ${number}`);
617 | return number;
618 | },
619 |
620 | /**
621 | * Reformats date string into string
622 | * @param dateString
623 | * @returns {string}
624 | */
625 | reformatDateString(dateString) {
626 | const months = {
627 | '01': 'January',
628 | '02': 'February',
629 | '03': 'March',
630 | '04': 'April',
631 | '05': 'May',
632 | '06': 'June',
633 | '07': 'July',
634 | '08': 'August',
635 | '09': 'September',
636 | 10: 'October',
637 | 11: 'November',
638 | 12: 'December',
639 | };
640 | const b = dateString.split('/');
641 | return `${b[0]} ${months[b[1]]} ${b[2]}`;
642 | },
643 |
644 | /**
645 | * Sorts results by date
646 | * @param array
647 | * @returns {*}
648 | */
649 | sortByDate(array) {
650 | array.sort((a, b) => {
651 | const sentDateA = a.split('/');
652 | const c = new Date(sentDateA[2], sentDateA[1], sentDateA[0]);
653 | const sentDateB = b.split('/');
654 | const d = new Date(sentDateB[2], sentDateB[1], sentDateB[0]);
655 | return d - c;
656 | });
657 | return array;
658 | },
659 |
660 | filterItem: async (selector, itemToFilter) => {
661 | try {
662 | const elem = await browser.$(selector);
663 | await elem.waitForExist(DELAY_5s);
664 | await elem.waitForEnabled(DELAY_5s);
665 | await browser.pause(DELAY_500ms);
666 | await elem.click();
667 | await browser.setValue(itemToFilter);
668 | } catch (err) {
669 | console.error(err.message);
670 | throw err;
671 | }
672 | },
673 |
674 | filterItemAndClick: async (selector) => {
675 | try {
676 | await this.filterItem('itemToFilter');
677 | await browser.pause(DELAY_3s);
678 | const elem = await browser.$(selector);
679 | await elem.click();
680 | await browser.pause(DELAY_3s);
681 | } catch (err) {
682 | console.error(err.message);
683 | throw err;
684 | }
685 | },
686 |
687 | /**
688 | * This generates the Date for uploading and retrieving the reports from s3
689 | * @returns {Date}
690 | */
691 | formatDate() {
692 | const $today = new Date();
693 | let $yesterday = new Date($today);
694 | if (s3Date === true) {
695 | $yesterday.setDate($today.getDate()); // for testing sending today's report.
696 | } else {
697 | $yesterday.setDate($today.getDate() - 1); // Also send last night reports, setDate also supports negative values, which cause the month to rollover.
698 | }
699 | let $dd = $yesterday.getDate();
700 | let $mm = $yesterday.getMonth() + 1; // January is 0!
701 | const $yyyy = $yesterday.getFullYear();
702 | if ($dd < 10) {
703 | $dd = `0${$dd}`;
704 | }
705 | if ($mm < 10) {
706 | $mm = `0${$mm}`;
707 | }
708 | $yesterday = `${$yyyy}-${$mm}-${$dd}`;
709 | return $yesterday;
710 | },
711 |
712 | /**
713 | * this uploads a file from local system or project folder
714 | * @param selector
715 | * @param filePath
716 | * @returns {Promise}
717 | */
718 | fileUpload: async (selector, filePath) => {
719 | elem = await browser.$(selector);
720 | await elem.isExisting();
721 | const remoteFilePath = await browser.uploadFile(filePath);
722 | await elem.addValue(remoteFilePath);
723 | },
724 |
725 |
726 | switchWindowTabs: async (tabId) => {
727 | const handles = await browser.getWindowHandles();
728 | if (handles.length > tabId) {
729 | await browser.switchToWindow(handles[tabId]);
730 | await browser.pause(DELAY_1s);
731 | }
732 | },
733 |
734 | /**
735 | * Function to verify if a file has been downloaded
736 | * @param {string} fileName Filename with extension
737 | * @param {number} timeout Maximum wait time for the file to be downloaded, default value is set to 5 seconds
738 | * @param {number} interval Wait time between every iteration to recheck the file download, default value is set to 500 ms
739 | */
740 | async verifyDownload(fileName, timeout = DELAY_5s, interval = DELAY_500ms) {
741 | let value = settings.remoteService === 'lambdatest' ? 0 : 1;
742 | let path;
743 | if (value === 1) {
744 | // The below path points to the default downloads folder, if the folder is in some other location, it has to be configured.
745 | let home = os.homedir();
746 | path = home + `/Downloads/${fileName}`;
747 | }
748 | let isFileDownloaded = false;
749 | let timeoutInSeconds = timeout / DELAY_1s;
750 | let intervalInSeconds = interval / DELAY_1s;
751 |
752 | loop: for (let i = 1; i <= timeoutInSeconds / intervalInSeconds; i++) {
753 | switch (value) {
754 | case 0:
755 | if (await browser.execute(`lambda-file-exists=${fileName}`)) {
756 | isFileDownloaded = true;
757 | break loop;
758 | }
759 | break;
760 | case 1:
761 | if (fs.existsSync(path)) {
762 | isFileDownloaded = true;
763 | break loop;
764 | }
765 | }
766 | await browser.pause(interval);
767 | }
768 | assert.isTrue(isFileDownloaded, `File '${fileName}' is still not downloaded after ${timeout} ms`);
769 | },
770 |
771 | /**
772 | * Function to upload one or more files
773 | * @param {string|string[]} filePaths files to be uploaded with extension
774 | * @param {string} locator element having attribute type='file'
775 | */
776 | async uploadFiles(filePaths, locator) {
777 | if (typeof filePaths === 'string') {
778 | filePaths = [filePaths];
779 | } else if (!Array.isArray(filePaths)) {
780 | throw `Expected 'string|string[]' but '${typeof filePaths}' was passed`;
781 | } else if (filePaths.length === 0) {
782 | throw 'Empty array was passed';
783 | }
784 | elem = await browser.$(locator);
785 | await elem.waitForExist({ DELAY_5s });
786 | let remoteFilePath = [];
787 | for (let filePath of filePaths) {
788 | remoteFilePath.push(await browser.uploadFile(filePath));
789 | }
790 | await elem.addValue(remoteFilePath.join('\n'));
791 | },
792 |
793 | /**
794 | * Function to get the displayed element among multiple matches
795 | * @param {string} locator
796 | * @returns Displayed element
797 | */
798 | async returnDisplayedElement(locator) {
799 | elem = await browser.$(locator);
800 | await elem.waitForExist();
801 | let elems = await browser.$$(locator);
802 | for (let elem of elems) {
803 | if (await elem.isDisplayed()) return elem;
804 | }
805 | return null;
806 | },
807 | };
808 |
--------------------------------------------------------------------------------