simple-playwright-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fixtures/auth.contract.d.ts +17 -0
- package/dist/fixtures/auth.contract.js +2 -0
- package/dist/fixtures/auth.fixture.d.ts +2 -0
- package/dist/fixtures/auth.fixture.js +9 -0
- package/dist/fixtures/data.fixture.d.ts +5 -0
- package/dist/fixtures/data.fixture.js +35 -0
- package/dist/fixtures/envConfig.fixture.d.ts +7 -0
- package/dist/fixtures/envConfig.fixture.js +17 -0
- package/dist/fixtures/file.fixture.d.ts +7 -0
- package/dist/fixtures/file.fixture.js +10 -0
- package/dist/fixtures/index.d.ts +10 -0
- package/dist/fixtures/index.js +40 -0
- package/dist/fixtures/testrail.fixture.d.ts +5 -0
- package/dist/fixtures/testrail.fixture.js +10 -0
- package/dist/fixtures/types.d.ts +10 -0
- package/dist/fixtures/types.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +14 -0
- package/dist/loaders/data.loader.d.ts +8 -0
- package/dist/loaders/data.loader.js +50 -0
- package/dist/loaders/envConfig.loader.d.ts +2 -0
- package/dist/loaders/envConfig.loader.js +27 -0
- package/dist/loaders/scenario.loader.d.ts +2 -0
- package/dist/loaders/scenario.loader.js +78 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +6 -0
- package/dist/types/auth.d.ts +9 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/env.d.ts +12 -0
- package/dist/types/env.js +2 -0
- package/dist/types/fixtures.d.ts +12 -0
- package/dist/types/fixtures.js +2 -0
- package/dist/types/scenario.d.ts +5 -0
- package/dist/types/scenario.js +2 -0
- package/dist/types/testrail.d.ts +5 -0
- package/dist/types/testrail.js +2 -0
- package/dist/utils/auth-session/initAuthSession.d.ts +9 -0
- package/dist/utils/auth-session/initAuthSession.js +41 -0
- package/dist/utils/auth-session/storagePath.d.ts +1 -0
- package/dist/utils/auth-session/storagePath.js +14 -0
- package/dist/utils/auth-session/validateStore.d.ts +1 -0
- package/dist/utils/auth-session/validateStore.js +23 -0
- package/dist/utils/auth-session.d.ts +9 -0
- package/dist/utils/auth-session.js +64 -0
- package/dist/utils/auth-storage.d.ts +10 -0
- package/dist/utils/auth-storage.js +36 -0
- package/dist/utils/file-utils.d.ts +7 -0
- package/dist/utils/file-utils.js +34 -0
- package/dist/utils/testrail.client.d.ts +6 -0
- package/dist/utils/testrail.client.js +36 -0
- package/package.json +20 -0
- package/scripts/setup-framework.js +70 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
/**
|
|
3
|
+
* Contract for all login providers.
|
|
4
|
+
* Each project must implement this interface for its own login flow.
|
|
5
|
+
*/
|
|
6
|
+
export interface AuthProvider {
|
|
7
|
+
login(page: Page): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Contract for the authStorage block in environment configs.
|
|
11
|
+
* This is the only part of envConfig the framework cares about.
|
|
12
|
+
*/
|
|
13
|
+
export interface AuthStorageConfig {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
validityMinutes: number;
|
|
16
|
+
provider: string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dataFixture = void 0;
|
|
4
|
+
const data_loader_1 = require("../loaders/data.loader");
|
|
5
|
+
exports.dataFixture = {
|
|
6
|
+
td: async ({}, use, testInfo) => {
|
|
7
|
+
const envName = process.env.TEST_ENV || "qa";
|
|
8
|
+
const tag = process.env.SCENARIO_TAG;
|
|
9
|
+
console.log("I am inside Fixture");
|
|
10
|
+
let td;
|
|
11
|
+
try {
|
|
12
|
+
td = (0, data_loader_1.loadTestData)(testInfo, envName);
|
|
13
|
+
console.log("Loaded test data:", td, "Type:", Array.isArray(td) ? "array" : typeof td);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.error(`❌ loadTestData threw for env '${envName}':`, err);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
if (!td || (Array.isArray(td) && td.length === 0)) {
|
|
20
|
+
throw new Error(`❌ No test data found for ${testInfo.file} in environment '${envName}'`);
|
|
21
|
+
}
|
|
22
|
+
// ✅ Only filter if data is an array of scenarios
|
|
23
|
+
if (Array.isArray(td)) {
|
|
24
|
+
td = td.filter(sc => !tag || sc.tags?.includes(tag));
|
|
25
|
+
if (td.length === 0) {
|
|
26
|
+
throw new Error(`❌ No scenarios found for env="${envName}" with tag="${tag}"`);
|
|
27
|
+
}
|
|
28
|
+
console.log(`✅ Loaded ${td.length} scenarios [env=${envName}${tag ? `, tag=${tag}` : ""}]`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log("✅ Loaded test data object (no tag filtering applied)");
|
|
32
|
+
}
|
|
33
|
+
await use(td);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.envConfigFixture = void 0;
|
|
4
|
+
const envConfig_loader_1 = require("../loaders/envConfig.loader");
|
|
5
|
+
exports.envConfigFixture = {
|
|
6
|
+
envConfig: async ({ page }, use) => {
|
|
7
|
+
const config = (0, envConfig_loader_1.loadConfig)();
|
|
8
|
+
/*
|
|
9
|
+
if (config.autoLaunch && config.baseUrl) {
|
|
10
|
+
console.log(`🌐 Navigating to: ${config.baseUrl}`);
|
|
11
|
+
await page.goto(config.baseUrl, { waitUntil: "domcontentloaded" });
|
|
12
|
+
console.log("✅ Navigation complete");
|
|
13
|
+
}
|
|
14
|
+
*/
|
|
15
|
+
await use(config);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileFixture = void 0;
|
|
4
|
+
const file_utils_1 = require("../utils/file-utils");
|
|
5
|
+
exports.fileFixture = {
|
|
6
|
+
fileUtils: async ({ page }, use) => {
|
|
7
|
+
const utils = new file_utils_1.FileUtils(page);
|
|
8
|
+
await use(utils);
|
|
9
|
+
},
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
import { Fixtures } from "../types/fixtures";
|
|
3
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & Fixtures & {
|
|
4
|
+
authStore: (page: Page, creds: {
|
|
5
|
+
username: string;
|
|
6
|
+
password: string;
|
|
7
|
+
}, providerRegistry: Record<string, any>) => Promise<void>;
|
|
8
|
+
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
9
|
+
export { expect } from "@playwright/test";
|
|
10
|
+
export * from "../loaders/scenario.loader";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.expect = exports.test = void 0;
|
|
18
|
+
const test_1 = require("@playwright/test");
|
|
19
|
+
const envConfig_fixture_1 = require("./envConfig.fixture");
|
|
20
|
+
const data_fixture_1 = require("./data.fixture");
|
|
21
|
+
const testrail_fixture_1 = require("./testrail.fixture");
|
|
22
|
+
const initAuthSession_1 = require("../utils/auth-session/initAuthSession");
|
|
23
|
+
const file_fixture_1 = require("./file.fixture");
|
|
24
|
+
exports.test = test_1.test.extend({
|
|
25
|
+
...envConfig_fixture_1.envConfigFixture,
|
|
26
|
+
...data_fixture_1.dataFixture,
|
|
27
|
+
...testrail_fixture_1.testrailFixture,
|
|
28
|
+
...file_fixture_1.fileFixture,
|
|
29
|
+
authStore: async ({ envConfig }, use) => {
|
|
30
|
+
await use(async (page, creds, providerRegistry) => {
|
|
31
|
+
if (!envConfig.authStorage) {
|
|
32
|
+
throw new Error(`[Framework] authStorage block missing in envConfig for this environment`);
|
|
33
|
+
}
|
|
34
|
+
await (0, initAuthSession_1.initAuthSession)(page, envConfig.authStorage, creds, providerRegistry);
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
var test_2 = require("@playwright/test");
|
|
39
|
+
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_2.expect; } });
|
|
40
|
+
__exportStar(require("../loaders/scenario.loader"), exports); // ✅ keep scenarioLoader export
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.testrailFixture = void 0;
|
|
4
|
+
const testrail_client_1 = require("../utils/testrail.client");
|
|
5
|
+
exports.testrailFixture = {
|
|
6
|
+
testrail: async ({}, use) => {
|
|
7
|
+
const client = new testrail_client_1.TestRailClient(process.env.TESTRAIL_URL, process.env.TESTRAIL_USER, process.env.TESTRAIL_APIKEY);
|
|
8
|
+
await use(client);
|
|
9
|
+
},
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EnvConfig } from "../loaders/envConfig.loader";
|
|
2
|
+
import type { TestRailClient } from "../utils/testrail.client";
|
|
3
|
+
/**
|
|
4
|
+
* Central fixture contract for Playwright test.extend
|
|
5
|
+
*/
|
|
6
|
+
export type Fixtures = {
|
|
7
|
+
envConfig: EnvConfig;
|
|
8
|
+
td: any;
|
|
9
|
+
testrail: TestRailClient;
|
|
10
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { initAuthSession } from "./utils/auth-session/initAuthSession";
|
|
2
|
+
export { test, expect } from "./fixtures/index";
|
|
3
|
+
export { scenarioLoader } from "./loaders/scenario.loader";
|
|
4
|
+
export { loadConfig } from "./loaders/envConfig.loader";
|
|
5
|
+
export { FileUtils } from "./utils/file-utils";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileUtils = exports.loadConfig = exports.scenarioLoader = exports.expect = exports.test = exports.initAuthSession = void 0;
|
|
4
|
+
var initAuthSession_1 = require("./utils/auth-session/initAuthSession");
|
|
5
|
+
Object.defineProperty(exports, "initAuthSession", { enumerable: true, get: function () { return initAuthSession_1.initAuthSession; } });
|
|
6
|
+
var index_1 = require("./fixtures/index");
|
|
7
|
+
Object.defineProperty(exports, "test", { enumerable: true, get: function () { return index_1.test; } });
|
|
8
|
+
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return index_1.expect; } });
|
|
9
|
+
var scenario_loader_1 = require("./loaders/scenario.loader");
|
|
10
|
+
Object.defineProperty(exports, "scenarioLoader", { enumerable: true, get: function () { return scenario_loader_1.scenarioLoader; } });
|
|
11
|
+
var envConfig_loader_1 = require("./loaders/envConfig.loader");
|
|
12
|
+
Object.defineProperty(exports, "loadConfig", { enumerable: true, get: function () { return envConfig_loader_1.loadConfig; } });
|
|
13
|
+
var file_utils_1 = require("./utils/file-utils");
|
|
14
|
+
Object.defineProperty(exports, "FileUtils", { enumerable: true, get: function () { return file_utils_1.FileUtils; } });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TestInfo } from "@playwright/test";
|
|
2
|
+
/**
|
|
3
|
+
* Loads environment-specific test data for a given test file.
|
|
4
|
+
* - Resolves JSON file path based on test file location
|
|
5
|
+
* - Validates existence, non-empty content, and JSON format
|
|
6
|
+
* - Returns environment-specific slice of data
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadTestData(testInfo: TestInfo, envName: string): any;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadTestData = loadTestData;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Loads environment-specific test data for a given test file.
|
|
11
|
+
* - Resolves JSON file path based on test file location
|
|
12
|
+
* - Validates existence, non-empty content, and JSON format
|
|
13
|
+
* - Returns environment-specific slice of data
|
|
14
|
+
*/
|
|
15
|
+
function loadTestData(testInfo, envName) {
|
|
16
|
+
const projectRoot = process.cwd();
|
|
17
|
+
// Derive relative path from tests/ to the current test file
|
|
18
|
+
const rel = path_1.default.relative(path_1.default.join(projectRoot, "tests"), testInfo.file);
|
|
19
|
+
// Strip .spec.ts / .test.ts suffix
|
|
20
|
+
const fileBase = path_1.default.basename(rel)
|
|
21
|
+
.replace(/\.spec\.ts$/, "")
|
|
22
|
+
.replace(/\.test\.ts$/, "");
|
|
23
|
+
const dir = path_1.default.dirname(rel);
|
|
24
|
+
// Construct data file path
|
|
25
|
+
const dataPath = path_1.default.join(projectRoot, "data", dir, `${fileBase}.json`);
|
|
26
|
+
// Validate existence
|
|
27
|
+
if (!fs_1.default.existsSync(dataPath)) {
|
|
28
|
+
throw new Error(`\n❌ Test data file not found for test: ${fileBase}\nPath: ${dataPath}`);
|
|
29
|
+
}
|
|
30
|
+
// Validate non-empty
|
|
31
|
+
const raw = fs_1.default.readFileSync(dataPath, "utf-8").trim();
|
|
32
|
+
if (!raw) {
|
|
33
|
+
throw new Error(`\n❌ Test data file is empty for test: ${fileBase}\nPath: ${dataPath}`);
|
|
34
|
+
}
|
|
35
|
+
// Parse JSON
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`\n❌ Invalid JSON format in test data for: ${fileBase}\nPath: ${dataPath}`);
|
|
42
|
+
}
|
|
43
|
+
// Return environment-specific slice
|
|
44
|
+
if (parsed[envName]) {
|
|
45
|
+
return parsed[envName];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new Error(`\n❌ Data is not available for Execution Environment ${envName} in test data for: ${fileBase}\nPath: ${dataPath}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
// framework/src/loaders/envConfig.loader.ts
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
const configPath = path_1.default.join(process.cwd(), "config", "environments.json");
|
|
12
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
13
|
+
throw new Error(`❌ environments.json not found at ${configPath}`);
|
|
14
|
+
}
|
|
15
|
+
const raw = fs_1.default.readFileSync(configPath, "utf-8");
|
|
16
|
+
const allConfig = JSON.parse(raw);
|
|
17
|
+
const envName = process.env.TEST_ENV || "qa";
|
|
18
|
+
const defaults = allConfig.defaults || {};
|
|
19
|
+
const envConfig = allConfig[envName]; // ✅ flat lookup
|
|
20
|
+
if (!envConfig) {
|
|
21
|
+
throw new Error(`❌ Environment '${envName}' not defined in environments.json`);
|
|
22
|
+
}
|
|
23
|
+
if (!envConfig.baseUrl) {
|
|
24
|
+
throw new Error(`❌ Environment '${envName}' missing baseUrl`);
|
|
25
|
+
}
|
|
26
|
+
return { ...defaults, ...envConfig };
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.scenarioLoader = scenarioLoader;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function scenarioLoader(testFile, env = process.env.TEST_ENV || "qa", tag = process.env.SCENARIO_TAG || "") {
|
|
40
|
+
// Replace "tests" with "data" and change extension
|
|
41
|
+
const relPath = testFile.replace("tests", "data").replace(/\.spec\.ts$/, ".json");
|
|
42
|
+
const scenarioFile = path.resolve(relPath);
|
|
43
|
+
if (!fs.existsSync(scenarioFile)) {
|
|
44
|
+
throw new Error(`❌ Scenario file not found: ${scenarioFile}`);
|
|
45
|
+
}
|
|
46
|
+
const raw = fs.readFileSync(scenarioFile, "utf-8").trim();
|
|
47
|
+
if (!raw) {
|
|
48
|
+
throw new Error(`❌ Scenario file is empty: ${scenarioFile}`);
|
|
49
|
+
}
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(`❌ Invalid JSON format in scenario file: ${scenarioFile}`);
|
|
56
|
+
}
|
|
57
|
+
let scenarios;
|
|
58
|
+
// If top-level is an array, return it directly
|
|
59
|
+
if (Array.isArray(parsed)) {
|
|
60
|
+
scenarios = parsed;
|
|
61
|
+
}
|
|
62
|
+
// If top-level is an object keyed by env, return the matching array
|
|
63
|
+
else if (parsed[env] && Array.isArray(parsed[env])) {
|
|
64
|
+
scenarios = parsed[env];
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new Error(`❌ Scenario file must contain an array or an object with environment key "${env}"`);
|
|
68
|
+
}
|
|
69
|
+
// ✅ Strict tag filtering
|
|
70
|
+
if (tag) {
|
|
71
|
+
scenarios = scenarios.filter(sc => sc.tags?.includes(tag));
|
|
72
|
+
if (scenarios.length === 0) {
|
|
73
|
+
throw new Error(`❌ No scenarios found for env="${env}" with tag="${tag}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log(`✅ Loaded ${scenarios.length} scenarios [env=${env}${tag ? `, tag=${tag}` : ""}]`);
|
|
77
|
+
return scenarios;
|
|
78
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function log(message: string): void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EnvConfig } from "./env";
|
|
2
|
+
import type { TestrailType } from "./testrail";
|
|
3
|
+
import type { FileUtils } from "../utils/file-utils";
|
|
4
|
+
/**
|
|
5
|
+
* Central fixture contract for Playwright test.extend
|
|
6
|
+
*/
|
|
7
|
+
export type Fixtures = {
|
|
8
|
+
envConfig: EnvConfig;
|
|
9
|
+
td: any;
|
|
10
|
+
testrail: TestrailType;
|
|
11
|
+
fileUtils: FileUtils;
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
import { AuthProvider, AuthStorageConfig } from "../../types/auth";
|
|
3
|
+
export declare function initAuthSession(page: Page, authStorage: AuthStorageConfig | undefined, creds: {
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
}, providerRegistry: Record<string, new (creds: {
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}) => AuthProvider>): Promise<void>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initAuthSession = initAuthSession;
|
|
4
|
+
const storagePath_1 = require("./storagePath");
|
|
5
|
+
const validateStore_1 = require("./validateStore");
|
|
6
|
+
async function initAuthSession(page, authStorage, creds, providerRegistry) {
|
|
7
|
+
if (!authStorage?.enabled) {
|
|
8
|
+
console.log("[Framework] Auth storage disabled");
|
|
9
|
+
const ProviderClass = providerRegistry[authStorage?.provider ?? "OrangeHRMLogin"];
|
|
10
|
+
const authProvider = new ProviderClass(creds);
|
|
11
|
+
await authProvider.login(page);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const { validityMinutes, provider } = authStorage;
|
|
15
|
+
const envName = process.env.TEST_ENV || "default";
|
|
16
|
+
const storagePath = (0, storagePath_1.getStoragePath)(provider, envName, creds.username);
|
|
17
|
+
const ProviderClass = providerRegistry[provider];
|
|
18
|
+
if (!ProviderClass)
|
|
19
|
+
throw new Error(`[Framework] Unknown provider: ${provider}`);
|
|
20
|
+
const authProvider = new ProviderClass(creds);
|
|
21
|
+
let needsLogin = true;
|
|
22
|
+
if ((0, validateStore_1.isAuthStoreValid)(storagePath, validityMinutes)) {
|
|
23
|
+
console.log(`[Framework] Found valid auth store: ${storagePath}`);
|
|
24
|
+
const state = JSON.parse(require("fs").readFileSync(storagePath, "utf-8"));
|
|
25
|
+
await page.context().addCookies(state.cookies);
|
|
26
|
+
await page.reload();
|
|
27
|
+
if (!page.url().includes("/auth/login")) {
|
|
28
|
+
console.log(`[Framework] Reusing valid auth store`);
|
|
29
|
+
needsLogin = false;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log("[Framework] Auth store rejected, will re-login...");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (needsLogin) {
|
|
36
|
+
console.log("[Framework] Performing fresh login...");
|
|
37
|
+
await authProvider.login(page);
|
|
38
|
+
await page.context().storageState({ path: storagePath });
|
|
39
|
+
console.log(`[Framework] New auth store created: ${storagePath}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getStoragePath(provider: string, envName: string, username: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getStoragePath = getStoragePath;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function getStoragePath(provider, envName, username) {
|
|
10
|
+
const storageDir = path_1.default.resolve("storage");
|
|
11
|
+
if (!fs_1.default.existsSync(storageDir))
|
|
12
|
+
fs_1.default.mkdirSync(storageDir, { recursive: true });
|
|
13
|
+
return path_1.default.join(storageDir, `${provider}-${envName}-${username}-auth.json`);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isAuthStoreValid(storagePath: string, validityMinutes: number): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isAuthStoreValid = isAuthStoreValid;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
function isAuthStoreValid(storagePath, validityMinutes) {
|
|
9
|
+
if (!fs_1.default.existsSync(storagePath))
|
|
10
|
+
return false;
|
|
11
|
+
const raw = fs_1.default.readFileSync(storagePath, "utf-8").trim();
|
|
12
|
+
if (!raw)
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
const state = JSON.parse(raw);
|
|
16
|
+
const stats = fs_1.default.statSync(storagePath);
|
|
17
|
+
const ageMinutes = (Date.now() - stats.mtimeMs) / 60000;
|
|
18
|
+
return (state.cookies?.length > 0 || state.origins?.length > 0) && ageMinutes < validityMinutes;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
import { AuthProvider, AuthStorageConfig } from "../fixtures/auth.contract";
|
|
3
|
+
export declare function initAuthSession(page: Page, authStorage: AuthStorageConfig | undefined, creds: {
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
}, providerRegistry: Record<string, new (creds: {
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}) => AuthProvider>): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initAuthSession = initAuthSession;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function initAuthSession(page, authStorage, creds, providerRegistry) {
|
|
10
|
+
if (!authStorage?.enabled) {
|
|
11
|
+
console.log("[Framework] Auth storage disabled");
|
|
12
|
+
const ProviderClass = providerRegistry[authStorage?.provider ?? "OrangeHRMLogin"];
|
|
13
|
+
const authProvider = new ProviderClass(creds);
|
|
14
|
+
await authProvider.login(page);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const { validityMinutes, provider } = authStorage;
|
|
18
|
+
const envName = process.env.TEST_ENV || "default"; // 👈 environment identifier
|
|
19
|
+
const storageDir = path_1.default.resolve("storage");
|
|
20
|
+
if (!fs_1.default.existsSync(storageDir))
|
|
21
|
+
fs_1.default.mkdirSync(storageDir, { recursive: true });
|
|
22
|
+
// 👇 include provider, environment, and username in filename
|
|
23
|
+
const storagePath = path_1.default.join(storageDir, `${provider}-${envName}-${creds.username}-auth.json`);
|
|
24
|
+
const ProviderClass = providerRegistry[provider];
|
|
25
|
+
if (!ProviderClass)
|
|
26
|
+
throw new Error(`[Framework] Unknown provider: ${provider}`);
|
|
27
|
+
const authProvider = new ProviderClass(creds);
|
|
28
|
+
let needsLogin = true;
|
|
29
|
+
if (fs_1.default.existsSync(storagePath)) {
|
|
30
|
+
const raw = fs_1.default.readFileSync(storagePath, "utf-8").trim();
|
|
31
|
+
if (raw) {
|
|
32
|
+
try {
|
|
33
|
+
const state = JSON.parse(raw);
|
|
34
|
+
const stats = fs_1.default.statSync(storagePath);
|
|
35
|
+
const ageMinutes = (Date.now() - stats.mtimeMs) / 60000;
|
|
36
|
+
if ((state.cookies?.length > 0 || state.origins?.length > 0) && ageMinutes < validityMinutes) {
|
|
37
|
+
console.log(`[Framework] Found non-empty auth store, validating...`);
|
|
38
|
+
await page.context().addCookies(state.cookies);
|
|
39
|
+
await page.reload();
|
|
40
|
+
// ✅ Validate by checking if redirected to login
|
|
41
|
+
if (!page.url().includes("/auth/login")) {
|
|
42
|
+
console.log(`[Framework] Reusing valid auth store: ${storagePath}`);
|
|
43
|
+
needsLogin = false;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log("[Framework] Auth store rejected by app, will re-login...");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log("[Framework] Auth store empty or expired, will re-login...");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.log("[Framework] Auth store corrupted, will re-login...");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (needsLogin) {
|
|
59
|
+
console.log("[Framework] Performing fresh login...");
|
|
60
|
+
await authProvider.login(page);
|
|
61
|
+
await page.context().storageState({ path: storagePath }); // overwrite
|
|
62
|
+
console.log(`[Framework] New auth store created: ${storagePath}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
import { AuthProvider, AuthStorageConfig } from ".//../fixtures/auth.contract";
|
|
3
|
+
export declare function initAuthSession(page: Page, authStorage: AuthStorageConfig, // 👈 typed against the contract
|
|
4
|
+
creds: {
|
|
5
|
+
username: string;
|
|
6
|
+
password: string;
|
|
7
|
+
}, providerRegistry: Record<string, new (creds: {
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}) => AuthProvider>): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initAuthSession = initAuthSession;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
// 👆 import both interfaces from the contract file
|
|
10
|
+
async function initAuthSession(page, authStorage, // 👈 typed against the contract
|
|
11
|
+
creds, providerRegistry) {
|
|
12
|
+
const { enabled, validityMinutes, provider } = authStorage;
|
|
13
|
+
const storageDir = path_1.default.resolve("storage");
|
|
14
|
+
if (!fs_1.default.existsSync(storageDir))
|
|
15
|
+
fs_1.default.mkdirSync(storageDir, { recursive: true });
|
|
16
|
+
const storagePath = path_1.default.join(storageDir, `${provider}-auth.json`);
|
|
17
|
+
const ProviderClass = providerRegistry[provider];
|
|
18
|
+
if (!ProviderClass)
|
|
19
|
+
throw new Error(`[Framework] Unknown provider: ${provider}`);
|
|
20
|
+
const authProvider = new ProviderClass(creds);
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
await authProvider.login(page);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (fs_1.default.existsSync(storagePath)) {
|
|
26
|
+
const stats = fs_1.default.statSync(storagePath);
|
|
27
|
+
const ageMinutes = (Date.now() - stats.mtimeMs) / 60000;
|
|
28
|
+
if (ageMinutes < validityMinutes) {
|
|
29
|
+
console.log(`[Framework] Reusing existing auth store: ${storagePath}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
await authProvider.login(page);
|
|
34
|
+
await page.context().storageState({ path: storagePath });
|
|
35
|
+
console.log(`[Framework] New auth store created: ${storagePath}`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FileUtils = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
class FileUtils {
|
|
10
|
+
page;
|
|
11
|
+
constructor(page) {
|
|
12
|
+
this.page = page;
|
|
13
|
+
}
|
|
14
|
+
// Upload a file by setting input[type=file]
|
|
15
|
+
async uploadFile(selector, filePath) {
|
|
16
|
+
const absolutePath = path_1.default.resolve(filePath);
|
|
17
|
+
if (!fs_1.default.existsSync(absolutePath)) {
|
|
18
|
+
throw new Error(`❌ File not found: ${absolutePath}`);
|
|
19
|
+
}
|
|
20
|
+
await this.page.setInputFiles(selector, absolutePath);
|
|
21
|
+
console.log(`✅ Uploaded file: ${absolutePath}`);
|
|
22
|
+
}
|
|
23
|
+
// Download a file triggered by clicking a link/button
|
|
24
|
+
async downloadFile(selector, downloadDir = "downloads") {
|
|
25
|
+
const downloadPromise = this.page.waitForEvent("download");
|
|
26
|
+
await this.page.click(selector);
|
|
27
|
+
const download = await downloadPromise;
|
|
28
|
+
const filePath = path_1.default.join(downloadDir, await download.suggestedFilename());
|
|
29
|
+
await download.saveAs(filePath);
|
|
30
|
+
console.log(`✅ File downloaded to: ${filePath}`);
|
|
31
|
+
return filePath;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.FileUtils = FileUtils;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TestRailClient = void 0;
|
|
7
|
+
// framework/src/utils/testrail.client.ts
|
|
8
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
9
|
+
// framework/src/utils/testrail.client.ts
|
|
10
|
+
class TestRailClient {
|
|
11
|
+
baseUrl;
|
|
12
|
+
authHeader;
|
|
13
|
+
constructor(baseUrl, username, apiKey) {
|
|
14
|
+
this.baseUrl = baseUrl || "";
|
|
15
|
+
this.authHeader = username && apiKey
|
|
16
|
+
? "Basic " + Buffer.from(`${username}:${apiKey}`).toString("base64")
|
|
17
|
+
: "";
|
|
18
|
+
}
|
|
19
|
+
async addResult(caseId, statusId, comment) {
|
|
20
|
+
if (!this.baseUrl) {
|
|
21
|
+
console.warn(`⚠️ Demo mode: would push result to TestRail case ${caseId} → status ${statusId}, comment: ${comment}`);
|
|
22
|
+
return { demo: true };
|
|
23
|
+
}
|
|
24
|
+
const url = `${this.baseUrl}/index.php?/api/v2/add_result_for_case/${caseId}`;
|
|
25
|
+
const response = await (0, node_fetch_1.default)(url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
"Authorization": this.authHeader,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ status_id: statusId, comment }),
|
|
32
|
+
});
|
|
33
|
+
return response.json();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.TestRailClient = TestRailClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simple-playwright-framework",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"clean": "rimraf dist .cache",
|
|
8
|
+
"build": "tsc -p tsconfig.json"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@playwright/test": "^1.58.2"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"init-sample-project": "./scripts/setup-framework.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"scripts"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
|
|
6
|
+
const rl = readline.createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function ask(question, defaultValue) {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
rl.question(`${question} (${defaultValue}): `, answer => {
|
|
14
|
+
resolve(answer.trim() || defaultValue);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
console.log("🚀 Init Sample Playwright Project");
|
|
21
|
+
|
|
22
|
+
const projectName = await ask("Project name", "sample-project");
|
|
23
|
+
const includeAuth = await ask("Include auth example? yes/no", "yes");
|
|
24
|
+
const includeData = await ask("Include data example? yes/no", "yes");
|
|
25
|
+
|
|
26
|
+
rl.close();
|
|
27
|
+
|
|
28
|
+
const projectDir = path.join(process.cwd(), projectName);
|
|
29
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Config
|
|
32
|
+
fs.mkdirSync(path.join(projectDir, "config"), { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
path.join(projectDir, "config", "environments.json"),
|
|
35
|
+
JSON.stringify({ qa: { baseUrl: "http://localhost:3000" } }, null, 2)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Tests
|
|
39
|
+
fs.mkdirSync(path.join(projectDir, "tests", "ui"), { recursive: true });
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(projectDir, "tests", "ui", "login.spec.ts"),
|
|
42
|
+
`import { test, expect } from '@playwright/test';
|
|
43
|
+
|
|
44
|
+
test('login example', async ({ page }) => {
|
|
45
|
+
await page.goto('http://localhost:3000/login');
|
|
46
|
+
await expect(page).toHaveTitle(/Login/);
|
|
47
|
+
});`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (includeAuth.toLowerCase() === "yes") {
|
|
51
|
+
fs.mkdirSync(path.join(projectDir, "auth"), { recursive: true });
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(projectDir, "auth", "index.ts"),
|
|
54
|
+
`export const creds = { username: "testuser", password: "testpass" };`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (includeData.toLowerCase() === "yes") {
|
|
59
|
+
fs.mkdirSync(path.join(projectDir, "data", "ui"), { recursive: true });
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(projectDir, "data", "ui", "sample.txt"),
|
|
62
|
+
"Sample test data"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`✅ Sample project created in ${projectDir}`);
|
|
67
|
+
console.log(`👉 Next steps:\n cd ${projectName}\n npx playwright test`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main();
|