flowstride 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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/adapters/api/undici.d.ts +46 -0
- package/dist/adapters/api/undici.js +118 -0
- package/dist/adapters/web/playwright.d.ts +51 -0
- package/dist/adapters/web/playwright.js +1108 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +77 -0
- package/dist/cli/commands/report.d.ts +1 -0
- package/dist/cli/commands/report.js +78 -0
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.js +158 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +39 -0
- package/dist/parser/lexer.d.ts +29 -0
- package/dist/parser/lexer.js +425 -0
- package/dist/parser/parser.d.ts +41 -0
- package/dist/parser/parser.js +573 -0
- package/dist/parser/resolver.d.ts +5 -0
- package/dist/parser/resolver.js +19 -0
- package/dist/parser/validator.d.ts +4 -0
- package/dist/parser/validator.js +37 -0
- package/dist/parser/variable_resolver.d.ts +7 -0
- package/dist/parser/variable_resolver.js +33 -0
- package/dist/runner/data_generators.d.ts +55 -0
- package/dist/runner/data_generators.js +233 -0
- package/dist/runner/dom_stripper.d.ts +4 -0
- package/dist/runner/dom_stripper.js +61 -0
- package/dist/runner/events.d.ts +15 -0
- package/dist/runner/events.js +20 -0
- package/dist/runner/flow_orchestrator.d.ts +15 -0
- package/dist/runner/flow_orchestrator.js +216 -0
- package/dist/runner/flow_worker.d.ts +53 -0
- package/dist/runner/flow_worker.js +1112 -0
- package/dist/runner/persistent_vault.d.ts +12 -0
- package/dist/runner/persistent_vault.js +84 -0
- package/dist/runner/plugin_manager.d.ts +13 -0
- package/dist/runner/plugin_manager.js +164 -0
- package/dist/runner/temporary_store.d.ts +8 -0
- package/dist/runner/temporary_store.js +22 -0
- package/dist/runner/worker_entry.d.ts +1 -0
- package/dist/runner/worker_entry.js +63 -0
- package/dist/sdk/api_proxy.d.ts +8 -0
- package/dist/sdk/api_proxy.js +28 -0
- package/dist/sdk/db_proxy.d.ts +3 -0
- package/dist/sdk/db_proxy.js +15 -0
- package/dist/sessions/manager.d.ts +17 -0
- package/dist/sessions/manager.js +74 -0
- package/dist/sessions/types.d.ts +39 -0
- package/dist/sessions/types.js +2 -0
- package/dist/sessions/vault.d.ts +8 -0
- package/dist/sessions/vault.js +69 -0
- package/dist/test_data/commerce.json +341 -0
- package/dist/test_data/geography.json +1298 -0
- package/dist/test_data/identities.json +1420 -0
- package/dist/test_data/phone_data.json +27 -0
- package/dist/test_data/utility.json +246 -0
- package/dist/types/index.d.ts +104 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/redactor.d.ts +5 -0
- package/dist/utils/redactor.js +53 -0
- package/package.json +54 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VariableResolver = void 0;
|
|
4
|
+
class VariableResolver {
|
|
5
|
+
store;
|
|
6
|
+
constructor(store) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
}
|
|
9
|
+
resolve(input) {
|
|
10
|
+
if (typeof input === "string") {
|
|
11
|
+
return this.interpolateString(input);
|
|
12
|
+
}
|
|
13
|
+
if (typeof input === "object" && input !== null) {
|
|
14
|
+
const resolvedObj = Array.isArray(input) ? [] : {};
|
|
15
|
+
for (const key in input) {
|
|
16
|
+
resolvedObj[key] = this.resolve(input[key]);
|
|
17
|
+
}
|
|
18
|
+
return resolvedObj;
|
|
19
|
+
}
|
|
20
|
+
return input;
|
|
21
|
+
}
|
|
22
|
+
interpolateString(text) {
|
|
23
|
+
return text.replace(/\{\{(.+?)\}\}/g, (match, varName) => {
|
|
24
|
+
const value = this.store.get(varName.trim());
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
console.warn(`⚠️ Warning: Variable "{{${varName}}}" not found in store.`);
|
|
27
|
+
return match;
|
|
28
|
+
}
|
|
29
|
+
return String(value);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.VariableResolver = VariableResolver;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare class DataGenerators {
|
|
2
|
+
private memoryBank;
|
|
3
|
+
private runIdContext;
|
|
4
|
+
private workerIdContext;
|
|
5
|
+
constructor(runId?: string, workerId?: string);
|
|
6
|
+
resolve(template: string): string;
|
|
7
|
+
private pickRandom;
|
|
8
|
+
uuid: () => `${string}-${string}-${string}-${string}-${string}`;
|
|
9
|
+
timestamp: () => string;
|
|
10
|
+
isoTimestamp: () => string;
|
|
11
|
+
randomIntStr: () => string;
|
|
12
|
+
randomNumber: () => string;
|
|
13
|
+
randomString: () => string;
|
|
14
|
+
randomAlpha: () => string;
|
|
15
|
+
randomAlphaNumeric: () => string;
|
|
16
|
+
randomFirstName: () => string;
|
|
17
|
+
randomLastName: () => string;
|
|
18
|
+
randomFullName: () => string;
|
|
19
|
+
randomUsername: () => string;
|
|
20
|
+
randomDisplayName: () => string;
|
|
21
|
+
randomJobTitle: () => string;
|
|
22
|
+
randomEmail: () => string;
|
|
23
|
+
randomDomain: () => string;
|
|
24
|
+
private generateLocalizedPhone;
|
|
25
|
+
randomPhone: () => string;
|
|
26
|
+
randomPhoneNG: () => string;
|
|
27
|
+
randomPassword: () => string;
|
|
28
|
+
randomOtp: () => string;
|
|
29
|
+
randomPin: () => string;
|
|
30
|
+
randomToken: () => string;
|
|
31
|
+
randomCountry: () => string;
|
|
32
|
+
randomState: () => string;
|
|
33
|
+
randomCity: () => string;
|
|
34
|
+
randomStreet: () => string;
|
|
35
|
+
randomAddress: () => string;
|
|
36
|
+
randomZipCode: () => string;
|
|
37
|
+
private generatePair;
|
|
38
|
+
private retrievePair;
|
|
39
|
+
today: () => string;
|
|
40
|
+
tomorrow: () => string;
|
|
41
|
+
yesterday: () => string;
|
|
42
|
+
currentYear: () => string;
|
|
43
|
+
currentMonth: () => string;
|
|
44
|
+
randomCompany: () => string;
|
|
45
|
+
randomCurrency: () => string;
|
|
46
|
+
randomAmount: () => string;
|
|
47
|
+
randomTransactionId: () => string;
|
|
48
|
+
maliciousString: () => string;
|
|
49
|
+
invalidEmail: () => string;
|
|
50
|
+
overflowString: () => string;
|
|
51
|
+
specialChars: () => string;
|
|
52
|
+
runId: () => string;
|
|
53
|
+
workerId: () => string;
|
|
54
|
+
getMap(): Record<string, () => string>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
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.DataGenerators = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const identities_json_1 = __importDefault(require("../test_data/identities.json"));
|
|
9
|
+
const geography_json_1 = __importDefault(require("../test_data/geography.json"));
|
|
10
|
+
const commerce_json_1 = __importDefault(require("../test_data/commerce.json"));
|
|
11
|
+
const utility_json_1 = __importDefault(require("../test_data/utility.json"));
|
|
12
|
+
const phone_data_json_1 = __importDefault(require("../test_data/phone_data.json"));
|
|
13
|
+
class DataGenerators {
|
|
14
|
+
memoryBank;
|
|
15
|
+
runIdContext;
|
|
16
|
+
workerIdContext;
|
|
17
|
+
constructor(runId = `RUN-${(0, node_crypto_1.randomBytes)(3).toString("hex")}`, workerId = "worker-1") {
|
|
18
|
+
this.memoryBank = new Map();
|
|
19
|
+
this.runIdContext = runId;
|
|
20
|
+
this.workerIdContext = workerId;
|
|
21
|
+
}
|
|
22
|
+
resolve(template) {
|
|
23
|
+
if (!template || typeof template !== "string")
|
|
24
|
+
return template;
|
|
25
|
+
let resolvedString = template;
|
|
26
|
+
const emailOverrideRegex = /([$@])randomEmail@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
|
27
|
+
resolvedString = resolvedString.replace(emailOverrideRegex, (match, mode, customDomain) => {
|
|
28
|
+
if (mode === "$") {
|
|
29
|
+
const freshEmail = this.randomEmail();
|
|
30
|
+
this.memoryBank.set("randomEmail", freshEmail);
|
|
31
|
+
const prefix = freshEmail.split("@")[0];
|
|
32
|
+
return `${prefix}@${customDomain}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (!this.memoryBank.has("randomEmail")) {
|
|
36
|
+
throw new Error(`Framework Error: You tried to retrieve @randomEmail@${customDomain}, but no email was previously generated in this test.`);
|
|
37
|
+
}
|
|
38
|
+
const lockedEmail = this.memoryBank.get("randomEmail");
|
|
39
|
+
const prefix = lockedEmail.split("@")[0];
|
|
40
|
+
return `${prefix}@${customDomain}`;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const map = this.getMap();
|
|
44
|
+
const variableRegex = /([$@])([a-zA-Z0-9_]+(?:\[[a-zA-Z]+\])?)/g;
|
|
45
|
+
resolvedString = resolvedString.replace(variableRegex, (match, mode, varName) => {
|
|
46
|
+
if (varName.startsWith("randomPair[")) {
|
|
47
|
+
const typeMatch = varName.match(/\[([a-zA-Z]+)\]/);
|
|
48
|
+
const type = typeMatch ? typeMatch[1] : null;
|
|
49
|
+
if (!type)
|
|
50
|
+
return match;
|
|
51
|
+
if (mode === "$") {
|
|
52
|
+
return this.generatePair(type);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return this.retrievePair(type, varName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const generator = map[varName];
|
|
59
|
+
if (generator) {
|
|
60
|
+
if (mode === "$") {
|
|
61
|
+
const freshValue = generator();
|
|
62
|
+
this.memoryBank.set(varName, freshValue);
|
|
63
|
+
return freshValue;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (!this.memoryBank.has(varName)) {
|
|
67
|
+
throw new Error(`Framework Error: You tried to retrieve @${varName}, but no value was previously generated for it in this test.`);
|
|
68
|
+
}
|
|
69
|
+
return this.memoryBank.get(varName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return match;
|
|
73
|
+
});
|
|
74
|
+
return resolvedString;
|
|
75
|
+
}
|
|
76
|
+
pickRandom(array) {
|
|
77
|
+
if (!array || array.length === 0)
|
|
78
|
+
return "";
|
|
79
|
+
return array[(0, node_crypto_1.randomInt)(0, array.length)];
|
|
80
|
+
}
|
|
81
|
+
uuid = () => (0, node_crypto_1.randomUUID)();
|
|
82
|
+
timestamp = () => Math.floor(Date.now() / 1000).toString();
|
|
83
|
+
isoTimestamp = () => new Date().toISOString();
|
|
84
|
+
randomIntStr = () => (0, node_crypto_1.randomInt)(1000, 99999).toString();
|
|
85
|
+
randomNumber = () => Math.floor(Math.random() * 1000000).toString();
|
|
86
|
+
randomString = () => (0, node_crypto_1.randomBytes)(4).toString("hex");
|
|
87
|
+
randomAlpha = () => Math.random()
|
|
88
|
+
.toString(36)
|
|
89
|
+
.replace(/[^a-z]+/g, "")
|
|
90
|
+
.substring(0, 6);
|
|
91
|
+
randomAlphaNumeric = () => Math.random().toString(36).substring(2, 10);
|
|
92
|
+
randomFirstName = () => this.pickRandom(identities_json_1.default.firstNames);
|
|
93
|
+
randomLastName = () => this.pickRandom(identities_json_1.default.lastNames);
|
|
94
|
+
randomFullName = () => `${this.randomFirstName()} ${this.randomLastName()}`;
|
|
95
|
+
randomUsername = () => `${this.randomFirstName().toLowerCase()}_flow_${this.randomIntStr()}`;
|
|
96
|
+
randomDisplayName = () => `${this.randomFirstName()} ${this.randomLastName().charAt(0)}.`;
|
|
97
|
+
randomJobTitle = () => this.pickRandom(identities_json_1.default.jobTitles);
|
|
98
|
+
randomEmail = () => {
|
|
99
|
+
const defaultDomain = identities_json_1.default.emailDomains[0];
|
|
100
|
+
return `flow_${this.randomIntStr()}@${defaultDomain}`;
|
|
101
|
+
};
|
|
102
|
+
randomDomain = () => identities_json_1.default.emailDomains[0];
|
|
103
|
+
generateLocalizedPhone(prefix, length) {
|
|
104
|
+
const firstDigit = (0, node_crypto_1.randomInt)(1, 9).toString();
|
|
105
|
+
let remainingDigits = "";
|
|
106
|
+
for (let i = 1; i < length; i++) {
|
|
107
|
+
remainingDigits += (0, node_crypto_1.randomInt)(0, 9).toString();
|
|
108
|
+
}
|
|
109
|
+
return `${prefix}${firstDigit}${remainingDigits}`;
|
|
110
|
+
}
|
|
111
|
+
randomPhone = () => {
|
|
112
|
+
const countryObj = this.pickRandom(geography_json_1.default.locations);
|
|
113
|
+
const isoCode = countryObj.iso;
|
|
114
|
+
const rules = phone_data_json_1.default[isoCode];
|
|
115
|
+
if (!rules)
|
|
116
|
+
return `+1${this.generateLocalizedPhone("", 10)}`;
|
|
117
|
+
return this.generateLocalizedPhone(rules.prefix, rules.length);
|
|
118
|
+
};
|
|
119
|
+
randomPhoneNG = () => `0${(0, node_crypto_1.randomInt)(7000000000, 9099999999)}`;
|
|
120
|
+
randomPassword = () => `P@ss${this.randomAlphaNumeric()}!`;
|
|
121
|
+
randomOtp = () => (0, node_crypto_1.randomInt)(100000, 999999).toString();
|
|
122
|
+
randomPin = () => (0, node_crypto_1.randomInt)(1000, 9999).toString();
|
|
123
|
+
randomToken = () => (0, node_crypto_1.randomBytes)(32).toString("base64").substring(0, 24);
|
|
124
|
+
randomCountry = () => {
|
|
125
|
+
const countryObj = this.pickRandom(geography_json_1.default.locations);
|
|
126
|
+
return countryObj.country;
|
|
127
|
+
};
|
|
128
|
+
randomState = () => {
|
|
129
|
+
const countryObj = this.pickRandom(geography_json_1.default.locations);
|
|
130
|
+
const stateObj = this.pickRandom(countryObj.states);
|
|
131
|
+
return stateObj.name;
|
|
132
|
+
};
|
|
133
|
+
randomCity = () => {
|
|
134
|
+
const countryObj = this.pickRandom(geography_json_1.default.locations);
|
|
135
|
+
const stateObj = this.pickRandom(countryObj.states);
|
|
136
|
+
return this.pickRandom(stateObj.cities);
|
|
137
|
+
};
|
|
138
|
+
randomStreet = () => `${(0, node_crypto_1.randomInt)(1, 99)} Admiralty Way`;
|
|
139
|
+
randomAddress = () => `${this.randomStreet()}, ${this.randomCity()}`;
|
|
140
|
+
randomZipCode = () => (0, node_crypto_1.randomInt)(10000, 99999).toString();
|
|
141
|
+
generatePair(type) {
|
|
142
|
+
const countryObj = this.pickRandom(geography_json_1.default.locations);
|
|
143
|
+
const stateObj = this.pickRandom(countryObj.states);
|
|
144
|
+
const city = this.pickRandom(stateObj.cities);
|
|
145
|
+
const triplet = {
|
|
146
|
+
Country: countryObj.country,
|
|
147
|
+
State: stateObj.name,
|
|
148
|
+
City: city,
|
|
149
|
+
};
|
|
150
|
+
this.memoryBank.set("lockedLocationPair", JSON.stringify(triplet));
|
|
151
|
+
return triplet[type] || "";
|
|
152
|
+
}
|
|
153
|
+
retrievePair(type, originalMatch) {
|
|
154
|
+
if (!this.memoryBank.has("lockedLocationPair")) {
|
|
155
|
+
throw new Error(`Framework Error: You tried to retrieve @${originalMatch}, but no location pair was previously generated in this test.`);
|
|
156
|
+
}
|
|
157
|
+
const lockedData = JSON.parse(this.memoryBank.get("lockedLocationPair"));
|
|
158
|
+
return lockedData[type] || "";
|
|
159
|
+
}
|
|
160
|
+
today = () => new Date().toISOString().split("T")[0];
|
|
161
|
+
tomorrow = () => {
|
|
162
|
+
const d = new Date();
|
|
163
|
+
d.setDate(d.getDate() + 1);
|
|
164
|
+
return d.toISOString().split("T")[0];
|
|
165
|
+
};
|
|
166
|
+
yesterday = () => {
|
|
167
|
+
const d = new Date();
|
|
168
|
+
d.setDate(d.getDate() - 1);
|
|
169
|
+
return d.toISOString().split("T")[0];
|
|
170
|
+
};
|
|
171
|
+
currentYear = () => new Date().getFullYear().toString();
|
|
172
|
+
currentMonth = () => (new Date().getMonth() + 1).toString().padStart(2, "0");
|
|
173
|
+
randomCompany = () => this.pickRandom(commerce_json_1.default.companies);
|
|
174
|
+
randomCurrency = () => this.pickRandom(commerce_json_1.default.currencies);
|
|
175
|
+
randomAmount = () => {
|
|
176
|
+
return (Math.random() * 10000).toFixed(2);
|
|
177
|
+
};
|
|
178
|
+
randomTransactionId = () => `TXN-${this.randomAlphaNumeric().toUpperCase()}`;
|
|
179
|
+
maliciousString = () => this.pickRandom(utility_json_1.default.maliciousStrings);
|
|
180
|
+
invalidEmail = () => this.pickRandom(utility_json_1.default.invalidEmails);
|
|
181
|
+
overflowString = () => this.pickRandom(utility_json_1.default.overflowStrings);
|
|
182
|
+
specialChars = () => this.pickRandom(utility_json_1.default.specialChars);
|
|
183
|
+
runId = () => this.runIdContext;
|
|
184
|
+
workerId = () => this.workerIdContext;
|
|
185
|
+
getMap() {
|
|
186
|
+
return {
|
|
187
|
+
uuid: this.uuid.bind(this),
|
|
188
|
+
timestamp: this.timestamp.bind(this),
|
|
189
|
+
isoTimestamp: this.isoTimestamp.bind(this),
|
|
190
|
+
randomInt: this.randomIntStr.bind(this),
|
|
191
|
+
randomNumber: this.randomNumber.bind(this),
|
|
192
|
+
randomString: this.randomString.bind(this),
|
|
193
|
+
randomAlpha: this.randomAlpha.bind(this),
|
|
194
|
+
randomAlphaNumeric: this.randomAlphaNumeric.bind(this),
|
|
195
|
+
randomFirstName: this.randomFirstName.bind(this),
|
|
196
|
+
randomLastName: this.randomLastName.bind(this),
|
|
197
|
+
randomFullName: this.randomFullName.bind(this),
|
|
198
|
+
randomUsername: this.randomUsername.bind(this),
|
|
199
|
+
randomDisplayName: this.randomDisplayName.bind(this),
|
|
200
|
+
randomJobTitle: this.randomJobTitle.bind(this),
|
|
201
|
+
randomEmail: this.randomEmail.bind(this),
|
|
202
|
+
randomPhone: this.randomPhone.bind(this),
|
|
203
|
+
randomPhoneNG: this.randomPhoneNG.bind(this),
|
|
204
|
+
randomDomain: this.randomDomain.bind(this),
|
|
205
|
+
randomPassword: this.randomPassword.bind(this),
|
|
206
|
+
randomOtp: this.randomOtp.bind(this),
|
|
207
|
+
randomPin: this.randomPin.bind(this),
|
|
208
|
+
randomToken: this.randomToken.bind(this),
|
|
209
|
+
randomCountry: this.randomCountry.bind(this),
|
|
210
|
+
randomState: this.randomState.bind(this),
|
|
211
|
+
randomCity: this.randomCity.bind(this),
|
|
212
|
+
randomStreet: this.randomStreet.bind(this),
|
|
213
|
+
randomAddress: this.randomAddress.bind(this),
|
|
214
|
+
randomZipCode: this.randomZipCode.bind(this),
|
|
215
|
+
today: this.today.bind(this),
|
|
216
|
+
tomorrow: this.tomorrow.bind(this),
|
|
217
|
+
yesterday: this.yesterday.bind(this),
|
|
218
|
+
currentYear: this.currentYear.bind(this),
|
|
219
|
+
currentMonth: this.currentMonth.bind(this),
|
|
220
|
+
randomCompany: this.randomCompany.bind(this),
|
|
221
|
+
randomAmount: this.randomAmount.bind(this),
|
|
222
|
+
randomCurrency: this.randomCurrency.bind(this),
|
|
223
|
+
randomTransactionId: this.randomTransactionId.bind(this),
|
|
224
|
+
maliciousString: this.maliciousString.bind(this),
|
|
225
|
+
invalidEmail: this.invalidEmail.bind(this),
|
|
226
|
+
overflowString: this.overflowString.bind(this),
|
|
227
|
+
specialChars: this.specialChars.bind(this),
|
|
228
|
+
runId: this.runId.bind(this),
|
|
229
|
+
workerId: this.workerId.bind(this),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.DataGenerators = DataGenerators;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOMStripper = void 0;
|
|
4
|
+
class DOMStripper {
|
|
5
|
+
static async getStrippedHTML(page) {
|
|
6
|
+
try {
|
|
7
|
+
const strippedHtml = await page.evaluate(() => {
|
|
8
|
+
const clone = document.documentElement.cloneNode(true);
|
|
9
|
+
const junkTags = [
|
|
10
|
+
"script",
|
|
11
|
+
"style",
|
|
12
|
+
"svg",
|
|
13
|
+
"path",
|
|
14
|
+
"noscript",
|
|
15
|
+
"meta",
|
|
16
|
+
"link",
|
|
17
|
+
"iframe",
|
|
18
|
+
"canvas",
|
|
19
|
+
"video",
|
|
20
|
+
"audio",
|
|
21
|
+
"picture",
|
|
22
|
+
];
|
|
23
|
+
junkTags.forEach((tag) => {
|
|
24
|
+
const elements = clone.querySelectorAll(tag);
|
|
25
|
+
elements.forEach((el) => el.remove());
|
|
26
|
+
});
|
|
27
|
+
const hiddenElements = clone.querySelectorAll('[hidden], [aria-hidden="true"], [style*="display: none"], [style*="visibility: hidden"]');
|
|
28
|
+
hiddenElements.forEach((el) => el.remove());
|
|
29
|
+
const removeComments = (node) => {
|
|
30
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
31
|
+
const child = node.childNodes[i];
|
|
32
|
+
if (child.nodeType === Node.COMMENT_NODE) {
|
|
33
|
+
child.remove();
|
|
34
|
+
}
|
|
35
|
+
else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
36
|
+
removeComments(child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
removeComments(clone);
|
|
41
|
+
const allElements = clone.querySelectorAll("*");
|
|
42
|
+
allElements.forEach((el) => {
|
|
43
|
+
if (el.tagName.toLowerCase() === "img") {
|
|
44
|
+
el.removeAttribute("src");
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
let rawHtml = clone.outerHTML;
|
|
48
|
+
rawHtml = rawHtml.replace(/[\n\r\t]+/g, " ");
|
|
49
|
+
rawHtml = rawHtml.replace(/\s{2,}/g, " ");
|
|
50
|
+
rawHtml = rawHtml.replace(/>\s+</g, "><");
|
|
51
|
+
return rawHtml.trim();
|
|
52
|
+
});
|
|
53
|
+
return strippedHtml;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.warn("[DOM Stripper] Failed to extract minified DOM:", error);
|
|
57
|
+
return "<html><body>[Error: Could not extract DOM state]</body></html>";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.DOMStripper = DOMStripper;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
export declare enum FlowstrideEvents {
|
|
3
|
+
RUN_START = "run:start",
|
|
4
|
+
RUN_END = "run:end",
|
|
5
|
+
FILE_START = "file:start",
|
|
6
|
+
FILE_END = "file:end",
|
|
7
|
+
SCENARIO_START = "scenario:start",
|
|
8
|
+
SCENARIO_END = "scenario:end",
|
|
9
|
+
STEP_START = "step:start",
|
|
10
|
+
STEP_PASS = "step:pass",
|
|
11
|
+
STEP_FAIL = "step:fail",
|
|
12
|
+
STEP_NOT_TESTED = "step:not_tested"
|
|
13
|
+
}
|
|
14
|
+
export declare class FlowstrideEmitter extends EventEmitter {
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowstrideEmitter = exports.FlowstrideEvents = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
var FlowstrideEvents;
|
|
6
|
+
(function (FlowstrideEvents) {
|
|
7
|
+
FlowstrideEvents["RUN_START"] = "run:start";
|
|
8
|
+
FlowstrideEvents["RUN_END"] = "run:end";
|
|
9
|
+
FlowstrideEvents["FILE_START"] = "file:start";
|
|
10
|
+
FlowstrideEvents["FILE_END"] = "file:end";
|
|
11
|
+
FlowstrideEvents["SCENARIO_START"] = "scenario:start";
|
|
12
|
+
FlowstrideEvents["SCENARIO_END"] = "scenario:end";
|
|
13
|
+
FlowstrideEvents["STEP_START"] = "step:start";
|
|
14
|
+
FlowstrideEvents["STEP_PASS"] = "step:pass";
|
|
15
|
+
FlowstrideEvents["STEP_FAIL"] = "step:fail";
|
|
16
|
+
FlowstrideEvents["STEP_NOT_TESTED"] = "step:not_tested";
|
|
17
|
+
})(FlowstrideEvents || (exports.FlowstrideEvents = FlowstrideEvents = {}));
|
|
18
|
+
class FlowstrideEmitter extends events_1.EventEmitter {
|
|
19
|
+
}
|
|
20
|
+
exports.FlowstrideEmitter = FlowstrideEmitter;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FlowWorker } from "./flow_worker";
|
|
2
|
+
import { FlowstrideConfig } from "../types";
|
|
3
|
+
export declare class FlowOrchestrator {
|
|
4
|
+
private worker;
|
|
5
|
+
private workerCount;
|
|
6
|
+
private config?;
|
|
7
|
+
private wss;
|
|
8
|
+
private httpServer;
|
|
9
|
+
private globalTestScenarios;
|
|
10
|
+
private stepStatusRegistry;
|
|
11
|
+
constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
|
|
12
|
+
private forwardToDashboard;
|
|
13
|
+
private executeTestRun;
|
|
14
|
+
start(targetPath: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.FlowOrchestrator = void 0;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const http = __importStar(require("http"));
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const ws_1 = require("ws");
|
|
45
|
+
class FlowOrchestrator {
|
|
46
|
+
worker;
|
|
47
|
+
workerCount;
|
|
48
|
+
config;
|
|
49
|
+
wss = null;
|
|
50
|
+
httpServer = null;
|
|
51
|
+
globalTestScenarios = [];
|
|
52
|
+
stepStatusRegistry = new Map();
|
|
53
|
+
constructor(worker, workerCount = 1, config) {
|
|
54
|
+
this.worker = worker;
|
|
55
|
+
this.workerCount = workerCount;
|
|
56
|
+
this.config = config;
|
|
57
|
+
this.worker.on("worker_event", (data) => {
|
|
58
|
+
this.forwardToDashboard(data.type, data.payload);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
forwardToDashboard(type, payload) {
|
|
62
|
+
if (this.wss) {
|
|
63
|
+
this.wss.clients.forEach((client) => {
|
|
64
|
+
if (client.readyState === 1)
|
|
65
|
+
client.send(JSON.stringify({ type, payload }));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async executeTestRun(targetPath) {
|
|
70
|
+
this.forwardToDashboard("SUITE_START", {});
|
|
71
|
+
let filesToProcess = [];
|
|
72
|
+
const stats = fs.statSync(targetPath);
|
|
73
|
+
if (stats.isDirectory()) {
|
|
74
|
+
filesToProcess = fs
|
|
75
|
+
.readdirSync(targetPath)
|
|
76
|
+
.filter((f) => f.endsWith(".flow"))
|
|
77
|
+
.map((f) => path.join(targetPath, f));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
filesToProcess = [targetPath];
|
|
81
|
+
}
|
|
82
|
+
this.globalTestScenarios = [];
|
|
83
|
+
this.stepStatusRegistry.clear();
|
|
84
|
+
if (this.workerCount > 1) {
|
|
85
|
+
console.log(chalk_1.default.magenta(`\n[Notice]: You requested ${this.workerCount} parallel workers, but distributed parallel execution is a Flowstride Enterprise feature.`));
|
|
86
|
+
console.log(chalk_1.default.magenta(`[Notice]: Visit flowstride.io to upgrade. Falling back to sequential execution...\n`));
|
|
87
|
+
}
|
|
88
|
+
console.log(chalk_1.default.gray(`\n[Orchestrator]: Executing in standard sequential mode...`));
|
|
89
|
+
const listener = (data) => {
|
|
90
|
+
if (data.type === "STEP_UPDATE") {
|
|
91
|
+
if (data.payload && data.payload.id && data.payload.fileName) {
|
|
92
|
+
const compositeKey = `${data.payload.fileName}_${data.payload.id}`;
|
|
93
|
+
this.stepStatusRegistry.set(compositeKey, data.payload.status);
|
|
94
|
+
}
|
|
95
|
+
this.globalTestScenarios.push(data.payload);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.worker.on("worker_event", listener);
|
|
99
|
+
await this.worker.runBatch(filesToProcess);
|
|
100
|
+
this.worker.off("worker_event", listener);
|
|
101
|
+
this.forwardToDashboard("RUN_COMPLETE", { status: "success" });
|
|
102
|
+
}
|
|
103
|
+
async start(targetPath) {
|
|
104
|
+
try {
|
|
105
|
+
this.httpServer = http.createServer((req, res) => {
|
|
106
|
+
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
|
|
107
|
+
if (req.url && req.url.startsWith("/videos/")) {
|
|
108
|
+
let urlObj;
|
|
109
|
+
try {
|
|
110
|
+
urlObj = new URL(req.url, `http://${req.headers.host}`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
res.writeHead(400);
|
|
114
|
+
return res.end("Bad Request");
|
|
115
|
+
}
|
|
116
|
+
const rawVideoName = decodeURIComponent(urlObj.pathname.replace("/videos/", ""));
|
|
117
|
+
const videoName = path.basename(rawVideoName);
|
|
118
|
+
if (!videoName || videoName === "/" || videoName === ".") {
|
|
119
|
+
res.writeHead(400);
|
|
120
|
+
return res.end("Invalid video requested");
|
|
121
|
+
}
|
|
122
|
+
const videoPath = path.join(process.cwd(), ".flowstride", "mock-cloud", videoName);
|
|
123
|
+
if (fs.existsSync(videoPath)) {
|
|
124
|
+
const stat = fs.statSync(videoPath);
|
|
125
|
+
const fileSize = stat.size;
|
|
126
|
+
if (fileSize === 0) {
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
"Content-Length": 0,
|
|
129
|
+
"Content-Type": "video/webm",
|
|
130
|
+
});
|
|
131
|
+
return res.end();
|
|
132
|
+
}
|
|
133
|
+
const range = req.headers.range;
|
|
134
|
+
if (range) {
|
|
135
|
+
const parts = range.replace(/bytes=/, "").split("-");
|
|
136
|
+
const start = parseInt(parts[0], 10);
|
|
137
|
+
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
|
138
|
+
const chunksize = end - start + 1;
|
|
139
|
+
const file = fs.createReadStream(videoPath, { start, end });
|
|
140
|
+
const head = {
|
|
141
|
+
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
|
|
142
|
+
"Accept-Ranges": "bytes",
|
|
143
|
+
"Content-Length": chunksize,
|
|
144
|
+
"Content-Type": "video/webm",
|
|
145
|
+
};
|
|
146
|
+
res.writeHead(206, head);
|
|
147
|
+
file.pipe(res);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const head = {
|
|
151
|
+
"Content-Length": fileSize,
|
|
152
|
+
"Content-Type": "video/webm",
|
|
153
|
+
};
|
|
154
|
+
res.writeHead(200, head);
|
|
155
|
+
fs.createReadStream(videoPath).pipe(res);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
res.writeHead(404);
|
|
160
|
+
res.end("Video not found");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
res.writeHead(404);
|
|
165
|
+
res.end();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
this.httpServer.on("error", (e) => {
|
|
169
|
+
if (e.code === "EADDRINUSE") {
|
|
170
|
+
console.error(chalk_1.default.red(`\n[Error]: Port 4321 is in use. Close the other terminal.`));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
|
|
175
|
+
this.wss.on("connection", (ws) => {
|
|
176
|
+
ws.on("message", async (message) => {
|
|
177
|
+
const data = JSON.parse(message.toString());
|
|
178
|
+
if (data.type === "EXPORT_PDF_REQ") {
|
|
179
|
+
console.log(chalk_1.default.magenta(`\n[Notice]: Exporting high-fidelity PDF reports is a Flowstride Enterprise feature.`));
|
|
180
|
+
console.log(chalk_1.default.magenta(`[Notice]: Visit flowstride.io to upgrade and unlock advanced reporting.\n`));
|
|
181
|
+
}
|
|
182
|
+
else if (data.type === "REPLAY_REQ") {
|
|
183
|
+
await this.worker.handleReplayRequest(data.payload);
|
|
184
|
+
}
|
|
185
|
+
else if (data.type === "RERUN_REQ") {
|
|
186
|
+
await this.executeTestRun(targetPath);
|
|
187
|
+
}
|
|
188
|
+
else if (data.type === "STOP_REQ") {
|
|
189
|
+
console.log(chalk_1.default.yellow("\nReceived STOP signal from Dashboard. Halting execution..."));
|
|
190
|
+
this.worker.abort();
|
|
191
|
+
await this.worker.shutdown();
|
|
192
|
+
this.forwardToDashboard("RUN_COMPLETE", { status: "aborted" });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
this.httpServer.listen(4321, async () => {
|
|
197
|
+
const { default: open } = await import("open");
|
|
198
|
+
await open("http://localhost:5173");
|
|
199
|
+
console.log(chalk_1.default.yellow("[Dashboard]: Waiting for connection..."));
|
|
200
|
+
while (this.wss.clients.size === 0)
|
|
201
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
202
|
+
console.log(chalk_1.default.green("[Dashboard]: Connected!"));
|
|
203
|
+
await this.executeTestRun(targetPath);
|
|
204
|
+
});
|
|
205
|
+
process.on("SIGINT", () => {
|
|
206
|
+
this.wss?.close();
|
|
207
|
+
this.httpServer?.close();
|
|
208
|
+
process.exit();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
exports.FlowOrchestrator = FlowOrchestrator;
|