@elliemae/smoked-suite 26.2.15
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/README.md +58 -0
- package/dist/cjs/auth/index.js +153 -0
- package/dist/cjs/base-page/index.js +58 -0
- package/dist/cjs/browserstack/index.js +97 -0
- package/dist/cjs/global-setup/index.js +71 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/monocartCoverage/index.js +125 -0
- package/dist/cjs/package.json +7 -0
- package/dist/cjs/page-setup/index.js +116 -0
- package/dist/cjs/playwright-config/index.js +116 -0
- package/dist/cjs/routes/index.js +27 -0
- package/dist/cjs/types.js +16 -0
- package/dist/esm/auth/index.js +123 -0
- package/dist/esm/base-page/index.js +38 -0
- package/dist/esm/browserstack/index.js +67 -0
- package/dist/esm/global-setup/index.js +41 -0
- package/dist/esm/index.js +20 -0
- package/dist/esm/monocartCoverage/index.js +95 -0
- package/dist/esm/package.json +7 -0
- package/dist/esm/page-setup/index.js +96 -0
- package/dist/esm/playwright-config/index.js +92 -0
- package/dist/esm/routes/index.js +7 -0
- package/dist/esm/types.js +0 -0
- package/dist/types/lib/auth/index.d.ts +41 -0
- package/dist/types/lib/base-page/index.d.ts +27 -0
- package/dist/types/lib/browserstack/index.d.ts +59 -0
- package/dist/types/lib/global-setup/index.d.ts +16 -0
- package/dist/types/lib/index.d.ts +10 -0
- package/dist/types/lib/monocartCoverage/index.d.ts +57 -0
- package/dist/types/lib/page-setup/index.d.ts +39 -0
- package/dist/types/lib/playwright-config/index.d.ts +3 -0
- package/dist/types/lib/routes/index.d.ts +16 -0
- package/dist/types/lib/types.d.ts +77 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var page_setup_exports = {};
|
|
20
|
+
__export(page_setup_exports, {
|
|
21
|
+
PageSetup: () => PageSetup
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(page_setup_exports);
|
|
24
|
+
class PageSetup {
|
|
25
|
+
/**
|
|
26
|
+
* Apply all page-level setup. Call once per test — each test
|
|
27
|
+
* receives a fresh Playwright page, so init scripts and route
|
|
28
|
+
* blocks do not carry over.
|
|
29
|
+
* @param page
|
|
30
|
+
*/
|
|
31
|
+
async apply(page) {
|
|
32
|
+
await Promise.all([
|
|
33
|
+
this.stubDiagnostics(page),
|
|
34
|
+
this.suppressErrorOverlays(page)
|
|
35
|
+
]);
|
|
36
|
+
await this.blockThirdPartyScripts(page);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Provide a no-op fallback for the emuiDiagnostics webpack external.
|
|
40
|
+
* @param page
|
|
41
|
+
*/
|
|
42
|
+
async stubDiagnostics(page) {
|
|
43
|
+
await page.addInitScript(() => {
|
|
44
|
+
if (typeof window.emuiDiagnostics === "undefined") {
|
|
45
|
+
const noop = () => {
|
|
46
|
+
};
|
|
47
|
+
const noopTransport = { log: async () => true };
|
|
48
|
+
const fallbackLogger = new Proxy({}, {
|
|
49
|
+
get(_target, prop) {
|
|
50
|
+
if (typeof prop === "string" && typeof console[prop] === "function") {
|
|
51
|
+
return console[prop].bind(console);
|
|
52
|
+
}
|
|
53
|
+
return prop === "setOptions" ? noop : console.log.bind(console);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
window.emuiDiagnostics = {
|
|
57
|
+
http: () => noopTransport,
|
|
58
|
+
logger: () => fallbackLogger,
|
|
59
|
+
Console: () => noopTransport,
|
|
60
|
+
logUnhandledErrors: noop,
|
|
61
|
+
webvitals: noop,
|
|
62
|
+
parentApp: () => noopTransport,
|
|
63
|
+
redactPii: (val) => val,
|
|
64
|
+
LogLevels: { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, FATAL: 4 }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Auto-hide webpack / react-error-overlay iframes that obscure
|
|
71
|
+
* the page during tests.
|
|
72
|
+
* @param page
|
|
73
|
+
*/
|
|
74
|
+
async suppressErrorOverlays(page) {
|
|
75
|
+
await page.addInitScript(() => {
|
|
76
|
+
const hideOverlayIframes = () => {
|
|
77
|
+
document.querySelectorAll("body > iframe").forEach((el) => {
|
|
78
|
+
el.style.display = "none";
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const start = () => {
|
|
82
|
+
hideOverlayIframes();
|
|
83
|
+
new MutationObserver(hideOverlayIframes).observe(document.body, {
|
|
84
|
+
childList: true
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
if (document.body) start();
|
|
88
|
+
else document.addEventListener("DOMContentLoaded", start);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Domains that are blocked during tests to speed up page loads. */
|
|
92
|
+
static BLOCKED_DOMAINS = [
|
|
93
|
+
"googletagmanager.com",
|
|
94
|
+
"google-analytics.com",
|
|
95
|
+
"fonts.googleapis.com",
|
|
96
|
+
"fonts.gstatic.com",
|
|
97
|
+
"hotjar.com",
|
|
98
|
+
"newrelic.com",
|
|
99
|
+
"nr-data.net",
|
|
100
|
+
"sentry.io",
|
|
101
|
+
"fullstory.com",
|
|
102
|
+
"pendo.io"
|
|
103
|
+
];
|
|
104
|
+
/** Single regex matching all {@link BLOCKED_DOMAINS}. */
|
|
105
|
+
static BLOCKED_PATTERN = new RegExp(
|
|
106
|
+
PageSetup.BLOCKED_DOMAINS.map((d) => d.replace(/\./g, "\\.")).join("|")
|
|
107
|
+
);
|
|
108
|
+
/**
|
|
109
|
+
* Block analytics, fonts, and monitoring scripts that slow page loads.
|
|
110
|
+
* Uses a single route handler instead of one per domain.
|
|
111
|
+
* @param page
|
|
112
|
+
*/
|
|
113
|
+
async blockThirdPartyScripts(page) {
|
|
114
|
+
await page.route(PageSetup.BLOCKED_PATTERN, (route) => route.abort());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var playwright_config_exports = {};
|
|
30
|
+
__export(playwright_config_exports, {
|
|
31
|
+
createPlaywrightConfig: () => createPlaywrightConfig
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(playwright_config_exports);
|
|
34
|
+
var path = __toESM(require("path"), 1);
|
|
35
|
+
var import_test = require("@playwright/test");
|
|
36
|
+
var import_global_setup = require("../global-setup/index.js");
|
|
37
|
+
var import_monocartCoverage = require("../monocartCoverage/index.js");
|
|
38
|
+
const DEFAULT_ACTION_TIMEOUT = 3e4;
|
|
39
|
+
const DEFAULT_NAVIGATION_TIMEOUT = 45e3;
|
|
40
|
+
const DEFAULT_TEST_TIMEOUT = 12e4;
|
|
41
|
+
const DEFAULT_EXPECT_TIMEOUT = 3e4;
|
|
42
|
+
const FAST_MODE_USE = {
|
|
43
|
+
trace: "off",
|
|
44
|
+
screenshot: "off",
|
|
45
|
+
video: "off"
|
|
46
|
+
};
|
|
47
|
+
const DEFAULT_USE = {
|
|
48
|
+
trace: "retain-on-failure",
|
|
49
|
+
screenshot: "only-on-failure",
|
|
50
|
+
video: "retain-on-failure"
|
|
51
|
+
};
|
|
52
|
+
const CI_CONFIG = {
|
|
53
|
+
workers: "100%",
|
|
54
|
+
forbidOnly: true,
|
|
55
|
+
retries: 2,
|
|
56
|
+
reporter: [
|
|
57
|
+
["list"],
|
|
58
|
+
["junit", { outputFile: "playwright-report/junit.xml" }]
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
const LOCAL_CONFIG = {
|
|
62
|
+
workers: "50%",
|
|
63
|
+
forbidOnly: false,
|
|
64
|
+
retries: 0,
|
|
65
|
+
reporter: [
|
|
66
|
+
["html", { outputFolder: "playwright-report" }],
|
|
67
|
+
["list"]
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
function createPlaywrightConfig(params = {}) {
|
|
71
|
+
const {
|
|
72
|
+
overrides = {},
|
|
73
|
+
globalAuth = true,
|
|
74
|
+
fastMode = false,
|
|
75
|
+
coverage
|
|
76
|
+
} = params;
|
|
77
|
+
const { use: useOverrides, expect: expectOverrides, ...rest } = overrides;
|
|
78
|
+
const isCoverage = !!coverage;
|
|
79
|
+
if (isCoverage) (0, import_monocartCoverage.configureCoverage)();
|
|
80
|
+
const globalSetup = path.resolve(__dirname, "../global-setup/index.js");
|
|
81
|
+
const globalAuthConfig = globalAuth ? { globalSetup } : {};
|
|
82
|
+
const storageStateConfig = globalAuth ? { storageState: path.join(process.cwd(), import_global_setup.STORAGE_FILE) } : {};
|
|
83
|
+
const envConfig = process.env.CI ? CI_CONFIG : LOCAL_CONFIG;
|
|
84
|
+
const coverageReporter = isCoverage ? { reporter: (0, import_monocartCoverage.buildCoverageReporters)(coverage) } : {};
|
|
85
|
+
return (0, import_test.defineConfig)({
|
|
86
|
+
testMatch: "**/*Spec.ts",
|
|
87
|
+
fullyParallel: true,
|
|
88
|
+
...envConfig,
|
|
89
|
+
...coverageReporter,
|
|
90
|
+
projects: [
|
|
91
|
+
{
|
|
92
|
+
name: "chromium",
|
|
93
|
+
use: { ...import_test.devices["Desktop Chrome"] }
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
outputDir: "test-results",
|
|
97
|
+
timeout: DEFAULT_TEST_TIMEOUT,
|
|
98
|
+
...globalAuthConfig,
|
|
99
|
+
...rest,
|
|
100
|
+
use: {
|
|
101
|
+
...fastMode ? FAST_MODE_USE : DEFAULT_USE,
|
|
102
|
+
viewport: { width: 1280, height: 720 },
|
|
103
|
+
actionTimeout: DEFAULT_ACTION_TIMEOUT,
|
|
104
|
+
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT,
|
|
105
|
+
...storageStateConfig,
|
|
106
|
+
...useOverrides,
|
|
107
|
+
...{
|
|
108
|
+
baseURL: process.env.BASE_URL ?? useOverrides?.baseURL ?? "http://localhost:3000"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
expect: {
|
|
112
|
+
timeout: DEFAULT_EXPECT_TIMEOUT,
|
|
113
|
+
...expectOverrides
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var routes_exports = {};
|
|
20
|
+
__export(routes_exports, {
|
|
21
|
+
getRoutePath: () => getRoutePath
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(routes_exports);
|
|
24
|
+
function getRoutePath(routes, route) {
|
|
25
|
+
if (route in routes) return routes[route];
|
|
26
|
+
return route;
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
var types_exports = {};
|
|
16
|
+
module.exports = __toCommonJS(types_exports);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
const AUTH_CAPTURE_TIMEOUT = 3e4;
|
|
4
|
+
const LOGIN_PAGE_TIMEOUT = 9e4;
|
|
5
|
+
const LOGIN_FIELD_TIMEOUT = 6e4;
|
|
6
|
+
const LOGIN_BUTTON_TIMEOUT = 3e4;
|
|
7
|
+
const AUTH_SHELL_TIMEOUT = 12e4;
|
|
8
|
+
const AUTH_TTL_MS = 25 * 60 * 1e3;
|
|
9
|
+
const AUTH_FILE = ".smoked-suite-auth.json";
|
|
10
|
+
class AuthManager {
|
|
11
|
+
static authState = null;
|
|
12
|
+
/** Whether a cached session exists and is still valid. */
|
|
13
|
+
hasValidAuth() {
|
|
14
|
+
const state = AuthManager.authState;
|
|
15
|
+
if (!state) return false;
|
|
16
|
+
return Date.now() - state.capturedAt < AUTH_TTL_MS;
|
|
17
|
+
}
|
|
18
|
+
/** Expose current auth state (used by global setup to serialise). */
|
|
19
|
+
static getAuthState() {
|
|
20
|
+
return AuthManager.authState;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load auth state from the file written by globalSetup.
|
|
24
|
+
* Returns `true` if a valid (non-expired) session was loaded.
|
|
25
|
+
*/
|
|
26
|
+
static loadFromFile() {
|
|
27
|
+
const filePath = path.join(process.cwd(), AUTH_FILE);
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(filePath)) return false;
|
|
30
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
31
|
+
const state = JSON.parse(raw);
|
|
32
|
+
if (!state?.sessionEntries || !state?.capturedAt) return false;
|
|
33
|
+
if (Date.now() - state.capturedAt >= AUTH_TTL_MS) return false;
|
|
34
|
+
AuthManager.authState = state;
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Restore every session-storage entry that was captured after
|
|
42
|
+
* the initial login so encw and its micro-apps find the data
|
|
43
|
+
* they need (Authorization, cred, user, userSettings, etc.).
|
|
44
|
+
* @param page
|
|
45
|
+
*/
|
|
46
|
+
async inject(page) {
|
|
47
|
+
const state = AuthManager.authState;
|
|
48
|
+
if (!state) {
|
|
49
|
+
throw new Error("No auth state available to inject");
|
|
50
|
+
}
|
|
51
|
+
await page.evaluate((entries) => {
|
|
52
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
53
|
+
sessionStorage.setItem(key, value);
|
|
54
|
+
}
|
|
55
|
+
}, state.sessionEntries);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot every session-storage entry after a successful login.
|
|
59
|
+
* @param page
|
|
60
|
+
*/
|
|
61
|
+
async capture(page) {
|
|
62
|
+
await page.waitForFunction(
|
|
63
|
+
() => sessionStorage.getItem("Authorization") !== null,
|
|
64
|
+
{ timeout: AUTH_CAPTURE_TIMEOUT }
|
|
65
|
+
);
|
|
66
|
+
const sessionEntries = await page.evaluate(() => {
|
|
67
|
+
const entries = {};
|
|
68
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
69
|
+
const key = sessionStorage.key(i);
|
|
70
|
+
if (key) entries[key] = sessionStorage.getItem(key) ?? "";
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
});
|
|
74
|
+
if (sessionEntries?.Authorization) {
|
|
75
|
+
AuthManager.authState = { sessionEntries, capturedAt: Date.now() };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Perform the encw login flow (Instance ID -> User / Password).
|
|
80
|
+
* @param page - Playwright page to drive.
|
|
81
|
+
* @param creds - Credentials to use.
|
|
82
|
+
*/
|
|
83
|
+
async login(page, creds) {
|
|
84
|
+
await page.goto("", { waitUntil: "load", timeout: LOGIN_PAGE_TIMEOUT });
|
|
85
|
+
const instanceIdField = page.getByPlaceholder("Instance ID");
|
|
86
|
+
await instanceIdField.waitFor({
|
|
87
|
+
state: "visible",
|
|
88
|
+
timeout: LOGIN_PAGE_TIMEOUT
|
|
89
|
+
});
|
|
90
|
+
await instanceIdField.click();
|
|
91
|
+
await instanceIdField.fill(creds.instanceId ?? "BE11226875");
|
|
92
|
+
const userIdField = page.getByPlaceholder("User ID");
|
|
93
|
+
await userIdField.waitFor({
|
|
94
|
+
state: "visible",
|
|
95
|
+
timeout: LOGIN_FIELD_TIMEOUT
|
|
96
|
+
});
|
|
97
|
+
await userIdField.click();
|
|
98
|
+
await userIdField.fill(creds.username);
|
|
99
|
+
await userIdField.press("Tab");
|
|
100
|
+
const passwordField = page.getByRole("textbox", { name: "Password" });
|
|
101
|
+
await passwordField.waitFor({
|
|
102
|
+
state: "visible",
|
|
103
|
+
timeout: LOGIN_BUTTON_TIMEOUT
|
|
104
|
+
});
|
|
105
|
+
await passwordField.fill(creds.password);
|
|
106
|
+
const loginButton = page.getByRole("button", { name: "Log In" });
|
|
107
|
+
await loginButton.waitFor({
|
|
108
|
+
state: "visible",
|
|
109
|
+
timeout: LOGIN_BUTTON_TIMEOUT
|
|
110
|
+
});
|
|
111
|
+
await loginButton.click();
|
|
112
|
+
await page.waitForLoadState("domcontentloaded", {
|
|
113
|
+
timeout: AUTH_SHELL_TIMEOUT
|
|
114
|
+
});
|
|
115
|
+
await page.waitForFunction(
|
|
116
|
+
() => sessionStorage.getItem("Authorization") !== null,
|
|
117
|
+
{ polling: 1e3, timeout: AUTH_SHELL_TIMEOUT }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
AuthManager
|
|
123
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class BasePage {
|
|
2
|
+
_contextProvider;
|
|
3
|
+
/** The current Page context, resolved lazily. */
|
|
4
|
+
get page() {
|
|
5
|
+
return typeof this._contextProvider === "function" ? this._contextProvider() : this._contextProvider;
|
|
6
|
+
}
|
|
7
|
+
constructor(context) {
|
|
8
|
+
this._contextProvider = context;
|
|
9
|
+
}
|
|
10
|
+
// ─── Locator helpers ─────────────────────────────────────────────────────────
|
|
11
|
+
getByTestId(testId) {
|
|
12
|
+
return this.page.getByTestId(testId);
|
|
13
|
+
}
|
|
14
|
+
locator(selector) {
|
|
15
|
+
return this.page.locator(selector);
|
|
16
|
+
}
|
|
17
|
+
getByRole(role, options) {
|
|
18
|
+
return this.page.getByRole(role, options);
|
|
19
|
+
}
|
|
20
|
+
getByText(text, options) {
|
|
21
|
+
return this.page.getByText(text, options);
|
|
22
|
+
}
|
|
23
|
+
getByLabel(text, options) {
|
|
24
|
+
return this.page.getByLabel(text, options);
|
|
25
|
+
}
|
|
26
|
+
getByPlaceholder(text, options) {
|
|
27
|
+
return this.page.getByPlaceholder(text, options);
|
|
28
|
+
}
|
|
29
|
+
getByAltText(text, options) {
|
|
30
|
+
return this.page.getByAltText(text, options);
|
|
31
|
+
}
|
|
32
|
+
getByTitle(text, options) {
|
|
33
|
+
return this.page.getByTitle(text, options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
BasePage
|
|
38
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
const DEFAULT_PLATFORMS = [
|
|
4
|
+
{
|
|
5
|
+
os: "OS X",
|
|
6
|
+
osVersion: "Sequoia",
|
|
7
|
+
browserName: "chrome",
|
|
8
|
+
browserVersion: "latest"
|
|
9
|
+
}
|
|
10
|
+
];
|
|
11
|
+
function createBrowserStackConfig(options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
projectName = "encw-app",
|
|
14
|
+
buildName = "playwright-tests",
|
|
15
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
16
|
+
buildIdentifier = "#${BUILD_NUMBER}",
|
|
17
|
+
platforms = DEFAULT_PLATFORMS,
|
|
18
|
+
parallelsPerPlatform = 1,
|
|
19
|
+
browserstackLocal = true,
|
|
20
|
+
forceLocal = true,
|
|
21
|
+
debug = true,
|
|
22
|
+
networkLogs = true,
|
|
23
|
+
consoleLogs = "info"
|
|
24
|
+
} = options;
|
|
25
|
+
const platformsYaml = platforms.map((p) => {
|
|
26
|
+
const lines = [
|
|
27
|
+
` - os: ${p.os}`,
|
|
28
|
+
` osVersion: ${p.osVersion}`,
|
|
29
|
+
` browserName: ${p.browserName}`
|
|
30
|
+
];
|
|
31
|
+
if (p.browserVersion) {
|
|
32
|
+
lines.push(` browserVersion: ${p.browserVersion}`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}).join("\n");
|
|
36
|
+
return `userName: "\${BROWSERSTACK_USERNAME}"
|
|
37
|
+
accessKey: "\${BROWSERSTACK_ACCESS_KEY}"
|
|
38
|
+
|
|
39
|
+
projectName: ${projectName}
|
|
40
|
+
buildName: ${buildName}
|
|
41
|
+
buildIdentifier: "${buildIdentifier}"
|
|
42
|
+
framework: playwright
|
|
43
|
+
|
|
44
|
+
platforms:
|
|
45
|
+
${platformsYaml}
|
|
46
|
+
|
|
47
|
+
parallelsPerPlatform: ${parallelsPerPlatform}
|
|
48
|
+
|
|
49
|
+
browserstackLocal: ${String(browserstackLocal)}
|
|
50
|
+
browserStackLocalOptions:
|
|
51
|
+
forceLocal: ${String(forceLocal)}
|
|
52
|
+
|
|
53
|
+
debug: ${String(debug)}
|
|
54
|
+
networkLogs: ${String(networkLogs)}
|
|
55
|
+
consoleLogs: ${consoleLogs}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
function writeBrowserStackConfig(options = {}, targetDir = process.cwd()) {
|
|
59
|
+
const content = createBrowserStackConfig(options);
|
|
60
|
+
const filePath = path.join(targetDir, "browserstack.yml");
|
|
61
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
62
|
+
return filePath;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
createBrowserStackConfig,
|
|
66
|
+
writeBrowserStackConfig
|
|
67
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { chromium } from "@playwright/test";
|
|
4
|
+
import { AuthManager } from "../auth/index.js";
|
|
5
|
+
import { PageSetup } from "../page-setup/index.js";
|
|
6
|
+
const AUTH_FILE = ".smoked-suite-auth.json";
|
|
7
|
+
const STORAGE_FILE = ".smoked-suite-storage.json";
|
|
8
|
+
async function globalSetup(config) {
|
|
9
|
+
const baseURL = config.projects[0]?.use?.baseURL ?? process.env.BASE_URL;
|
|
10
|
+
if (!baseURL) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"smoked-suite globalSetup: baseURL is required. Set BASE_URL env var or configure use.baseURL in createPlaywrightConfig."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const browser = await chromium.launch();
|
|
16
|
+
const context = await browser.newContext({ baseURL });
|
|
17
|
+
const page = await context.newPage();
|
|
18
|
+
const pageSetup = new PageSetup();
|
|
19
|
+
await pageSetup.apply(page);
|
|
20
|
+
const auth = new AuthManager();
|
|
21
|
+
const credentials = {
|
|
22
|
+
instanceId: process.env.ENCW_INSTANCE_ID ?? "BE11226875",
|
|
23
|
+
username: process.env.ENCW_USER_ID ?? "admin",
|
|
24
|
+
password: process.env.ENCW_PASSWORD ?? "Password#!23"
|
|
25
|
+
};
|
|
26
|
+
await auth.login(page, credentials);
|
|
27
|
+
await auth.capture(page);
|
|
28
|
+
const authState = AuthManager.getAuthState();
|
|
29
|
+
if (authState) {
|
|
30
|
+
const authPath = path.join(process.cwd(), AUTH_FILE);
|
|
31
|
+
fs.writeFileSync(authPath, JSON.stringify(authState), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
const storagePath = path.join(process.cwd(), STORAGE_FILE);
|
|
34
|
+
await context.storageState({ path: storagePath });
|
|
35
|
+
await browser.close();
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
AUTH_FILE,
|
|
39
|
+
STORAGE_FILE,
|
|
40
|
+
globalSetup as default
|
|
41
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BasePage } from "./base-page/index.js";
|
|
2
|
+
import { AuthManager } from "./auth/index.js";
|
|
3
|
+
import { PageSetup } from "./page-setup/index.js";
|
|
4
|
+
import { getRoutePath } from "./routes/index.js";
|
|
5
|
+
import { createPlaywrightConfig } from "./playwright-config/index.js";
|
|
6
|
+
import { default as default2 } from "./global-setup/index.js";
|
|
7
|
+
import {
|
|
8
|
+
createBrowserStackConfig,
|
|
9
|
+
writeBrowserStackConfig
|
|
10
|
+
} from "./browserstack/index.js";
|
|
11
|
+
export {
|
|
12
|
+
AuthManager,
|
|
13
|
+
BasePage,
|
|
14
|
+
PageSetup,
|
|
15
|
+
createBrowserStackConfig,
|
|
16
|
+
createPlaywrightConfig,
|
|
17
|
+
getRoutePath,
|
|
18
|
+
default2 as globalSetup,
|
|
19
|
+
writeBrowserStackConfig
|
|
20
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { test } from "@playwright/test";
|
|
2
|
+
let coverageConfigured = false;
|
|
3
|
+
let coverageEnabled = false;
|
|
4
|
+
const trackedPages = /* @__PURE__ */ new Set();
|
|
5
|
+
function configureCoverage() {
|
|
6
|
+
coverageConfigured = true;
|
|
7
|
+
}
|
|
8
|
+
function startCoverage() {
|
|
9
|
+
if (!coverageConfigured) return;
|
|
10
|
+
coverageEnabled = true;
|
|
11
|
+
trackedPages.clear();
|
|
12
|
+
}
|
|
13
|
+
async function stopCoverage() {
|
|
14
|
+
if (!coverageEnabled) return;
|
|
15
|
+
await Promise.allSettled(
|
|
16
|
+
[...trackedPages].map((page) => flushPageCoverage(page))
|
|
17
|
+
);
|
|
18
|
+
trackedPages.clear();
|
|
19
|
+
coverageEnabled = false;
|
|
20
|
+
}
|
|
21
|
+
async function trackPage(page) {
|
|
22
|
+
if (!coverageEnabled) return;
|
|
23
|
+
trackedPages.add(page);
|
|
24
|
+
await page.coverage.startJSCoverage({ resetOnNavigation: false });
|
|
25
|
+
}
|
|
26
|
+
async function untrackPage(page) {
|
|
27
|
+
if (!coverageEnabled || !trackedPages.has(page)) return;
|
|
28
|
+
await flushPageCoverage(page);
|
|
29
|
+
trackedPages.delete(page);
|
|
30
|
+
}
|
|
31
|
+
async function flushPageCoverage(page) {
|
|
32
|
+
const entries = await page.coverage.stopJSCoverage();
|
|
33
|
+
const { addCoverageReport } = await import("monocart-reporter");
|
|
34
|
+
await addCoverageReport(entries, test.info());
|
|
35
|
+
}
|
|
36
|
+
function buildCoverageReporter(configs) {
|
|
37
|
+
const repoName = process.cwd().split("/").pop() ?? "";
|
|
38
|
+
const segments = configs.map(
|
|
39
|
+
(c) => c.sourceDir.replace(/^\.\//, "").replace(/\/$/, "")
|
|
40
|
+
);
|
|
41
|
+
const firstBase = configs[0].sourceDir.split("/").filter(Boolean).pop() ?? "app";
|
|
42
|
+
const reportName = configs.length === 1 ? `${toTitleCase(firstBase)} Coverage Report` : "Coverage Report";
|
|
43
|
+
const outputDir = configs[0].outputDir ?? `./coverage/${firstBase}`;
|
|
44
|
+
const fileFilter = {
|
|
45
|
+
"**/*.{ts,tsx}": true,
|
|
46
|
+
"**/*.test.*": false,
|
|
47
|
+
"**/*.spec.*": false,
|
|
48
|
+
"**/tests/**": false
|
|
49
|
+
};
|
|
50
|
+
const allEntries = configs.map((c) => ({
|
|
51
|
+
dir: c.sourceDir,
|
|
52
|
+
filter: fileFilter
|
|
53
|
+
}));
|
|
54
|
+
return [
|
|
55
|
+
"monocart-reporter",
|
|
56
|
+
{
|
|
57
|
+
name: reportName,
|
|
58
|
+
outputFile: `${outputDir}/report.html`,
|
|
59
|
+
coverage: {
|
|
60
|
+
reports: ["v8", "console-details", "lcovonly"],
|
|
61
|
+
all: allEntries.length === 1 ? allEntries[0] : allEntries,
|
|
62
|
+
sourcePath: (filePath) => {
|
|
63
|
+
let p = filePath;
|
|
64
|
+
if (repoName) {
|
|
65
|
+
p = p.replace(new RegExp(`^${repoName}/`), "");
|
|
66
|
+
}
|
|
67
|
+
p = p.replace(/-[0-9a-f]{4}$/, "");
|
|
68
|
+
return p;
|
|
69
|
+
},
|
|
70
|
+
sourceFilter: (sourcePath) => {
|
|
71
|
+
if (sourcePath.includes("node_modules")) return false;
|
|
72
|
+
return segments.some((seg) => sourcePath.includes(seg));
|
|
73
|
+
},
|
|
74
|
+
entryFilter: (entry) => {
|
|
75
|
+
if (entry.url.startsWith("http")) return false;
|
|
76
|
+
return segments.some((seg) => entry.url.includes(seg));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
function toTitleCase(s) {
|
|
83
|
+
return s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
84
|
+
}
|
|
85
|
+
function buildCoverageReporters(configs) {
|
|
86
|
+
return [["list"], buildCoverageReporter(configs)];
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
buildCoverageReporters,
|
|
90
|
+
configureCoverage,
|
|
91
|
+
startCoverage,
|
|
92
|
+
stopCoverage,
|
|
93
|
+
trackPage,
|
|
94
|
+
untrackPage
|
|
95
|
+
};
|