@elastic/synthetics 1.24.3 → 1.25.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/README.md +67 -0
- package/dist/common_types.d.ts +17 -0
- package/dist/common_types.d.ts.map +1 -1
- package/dist/core/gatherer.d.ts +4 -4
- package/dist/core/gatherer.d.ts.map +1 -1
- package/dist/core/gatherer.js +28 -7
- package/dist/core/gatherer.js.map +1 -1
- package/dist/core/globals.js +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +16 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/runner.d.ts +1 -1
- package/dist/core/runner.d.ts.map +1 -1
- package/dist/core/runner.js +86 -58
- package/dist/core/runner.js.map +1 -1
- package/dist/dsl/api-journey.d.ts +62 -0
- package/dist/dsl/api-journey.d.ts.map +1 -0
- package/dist/dsl/api-journey.js +55 -0
- package/dist/dsl/api-journey.js.map +1 -0
- package/dist/dsl/index.d.ts +1 -0
- package/dist/dsl/index.d.ts.map +1 -1
- package/dist/dsl/index.js +1 -0
- package/dist/dsl/index.js.map +1 -1
- package/dist/dsl/journey.d.ts +10 -2
- package/dist/dsl/journey.d.ts.map +1 -1
- package/dist/dsl/journey.js +11 -2
- package/dist/dsl/journey.js.map +1 -1
- package/dist/formatter/codegen.d.ts +69 -0
- package/dist/formatter/codegen.d.ts.map +1 -0
- package/dist/formatter/codegen.js +210 -0
- package/dist/formatter/codegen.js.map +1 -0
- package/dist/formatter/javascript.d.ts +6 -6
- package/dist/formatter/javascript.d.ts.map +1 -1
- package/dist/formatter/javascript.js +7 -7
- package/dist/formatter/javascript.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +66 -1
- package/dist/loader.js.map +1 -1
- package/dist/network-timings.d.ts +55 -0
- package/dist/network-timings.d.ts.map +1 -0
- package/dist/network-timings.js +107 -0
- package/dist/network-timings.js.map +1 -0
- package/dist/plugins/api-network.d.ts +39 -0
- package/dist/plugins/api-network.d.ts.map +1 -0
- package/dist/plugins/api-network.js +235 -0
- package/dist/plugins/api-network.js.map +1 -0
- package/dist/plugins/network.d.ts +0 -6
- package/dist/plugins/network.d.ts.map +1 -1
- package/dist/plugins/network.js +5 -70
- package/dist/plugins/network.js.map +1 -1
- package/dist/plugins/plugin-manager.d.ts +11 -4
- package/dist/plugins/plugin-manager.d.ts.map +1 -1
- package/dist/plugins/plugin-manager.js +36 -16
- package/dist/plugins/plugin-manager.js.map +1 -1
- package/dist/push/index.d.ts.map +1 -1
- package/dist/push/index.js +4 -0
- package/dist/push/index.js.map +1 -1
- package/dist/push/monitor.d.ts.map +1 -1
- package/dist/push/monitor.js +1 -1
- package/dist/push/monitor.js.map +1 -1
- package/dist/push/run-local.d.ts.map +1 -1
- package/dist/push/run-local.js +2 -1
- package/dist/push/run-local.js.map +1 -1
- package/dist/push/utils.d.ts +7 -0
- package/dist/push/utils.d.ts.map +1 -1
- package/dist/push/utils.js +15 -1
- package/dist/push/utils.js.map +1 -1
- package/dist/reporters/base.d.ts +2 -2
- package/dist/reporters/base.d.ts.map +1 -1
- package/dist/reporters/base.js.map +1 -1
- package/dist/reporters/index.d.ts +2 -2
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/json.d.ts +6 -2
- package/dist/reporters/json.d.ts.map +1 -1
- package/dist/reporters/json.js +68 -33
- package/dist/reporters/json.js.map +1 -1
- package/package.json +7 -7
- package/src/common_types.ts +22 -0
- package/src/core/gatherer.ts +41 -10
- package/src/core/globals.ts +1 -1
- package/src/core/index.ts +28 -0
- package/src/core/runner.ts +93 -67
- package/src/dsl/api-journey.ts +93 -0
- package/src/dsl/index.ts +1 -0
- package/src/dsl/journey.ts +21 -9
- package/src/formatter/codegen.ts +299 -0
- package/src/formatter/javascript.ts +1 -4
- package/src/index.ts +6 -0
- package/src/loader.ts +78 -2
- package/src/network-timings.ts +130 -0
- package/src/plugins/api-network.ts +252 -0
- package/src/plugins/network.ts +5 -87
- package/src/plugins/plugin-manager.ts +60 -19
- package/src/push/index.ts +11 -0
- package/src/push/monitor.ts +6 -2
- package/src/push/run-local.ts +2 -1
- package/src/push/utils.ts +15 -0
- package/src/reporters/base.ts +2 -1
- package/src/reporters/index.ts +2 -1
- package/src/reporters/json.ts +74 -41
- package/templates/README.md +3 -1
- package/templates/journeys/advanced-api-example-helpers.ts +79 -0
- package/templates/journeys/advanced-api-example.journey.ts +52 -0
- package/templates/journeys/api-example.journey.ts +35 -0
- package/templates/synthetics.config.ts +2 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2020-present, Elastic NV
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
* THE SOFTWARE.
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adapted from playwright-core's `server/codegen/javascript.ts` and
|
|
28
|
+
* `server/codegen/language.ts` (Apache-2.0, (c) Microsoft Corporation).
|
|
29
|
+
*
|
|
30
|
+
* Playwright >= 1.61 bundles its server code into `coreBundle.js` and no
|
|
31
|
+
* longer ships the codegen classes as importable modules, so the previous
|
|
32
|
+
* `playwright-core/lib/server/codegen/javascript` deep import no longer
|
|
33
|
+
* resolves. We only need the slice of the generator that
|
|
34
|
+
* `SyntheticsGenerator` extends (`_asLocator` + `_generateActionCall`) plus
|
|
35
|
+
* `JavaScriptFormatter`; the heavy locator/serialization helpers are still
|
|
36
|
+
* reachable through the bundle's exported `iso` namespace, so we reuse those
|
|
37
|
+
* directly instead of vendoring them too.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
41
|
+
const { iso } = require('playwright-core/lib/coreBundle') as {
|
|
42
|
+
iso: {
|
|
43
|
+
asLocator(language: string, selector: string): string;
|
|
44
|
+
escapeWithQuotes(text: string, char?: string): string;
|
|
45
|
+
formatObject(value: any, indent?: string): string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
49
|
+
|
|
50
|
+
const { asLocator, escapeWithQuotes, formatObject } = iso;
|
|
51
|
+
|
|
52
|
+
type SmartKeyboardModifier =
|
|
53
|
+
| 'Alt'
|
|
54
|
+
| 'Control'
|
|
55
|
+
| 'ControlOrMeta'
|
|
56
|
+
| 'Meta'
|
|
57
|
+
| 'Shift';
|
|
58
|
+
|
|
59
|
+
type MouseClickOptions = {
|
|
60
|
+
button?: 'left' | 'middle' | 'right';
|
|
61
|
+
modifiers?: SmartKeyboardModifier[];
|
|
62
|
+
clickCount?: number;
|
|
63
|
+
position?: { x: number; y: number };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type CodegenAction = {
|
|
67
|
+
name: string;
|
|
68
|
+
selector?: string;
|
|
69
|
+
url?: string;
|
|
70
|
+
key?: string;
|
|
71
|
+
text?: string;
|
|
72
|
+
value?: string;
|
|
73
|
+
files?: string[];
|
|
74
|
+
options?: string[];
|
|
75
|
+
modifiers?: number;
|
|
76
|
+
button?: 'left' | 'middle' | 'right';
|
|
77
|
+
clickCount?: number;
|
|
78
|
+
position?: { x: number; y: number };
|
|
79
|
+
substring?: boolean;
|
|
80
|
+
checked?: boolean;
|
|
81
|
+
ariaSnapshot?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type CodegenActionInContext = {
|
|
85
|
+
action: CodegenAction;
|
|
86
|
+
frame: { pageAlias: string; framePath: string[] };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function toKeyboardModifiers(modifiers = 0): SmartKeyboardModifier[] {
|
|
90
|
+
const result: SmartKeyboardModifier[] = [];
|
|
91
|
+
if (modifiers & 1) result.push('Alt');
|
|
92
|
+
if (modifiers & 2) result.push('ControlOrMeta');
|
|
93
|
+
if (modifiers & 4) result.push('ControlOrMeta');
|
|
94
|
+
if (modifiers & 8) result.push('Shift');
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toClickOptionsForSourceCode(action: CodegenAction): MouseClickOptions {
|
|
99
|
+
const modifiers = toKeyboardModifiers(action.modifiers);
|
|
100
|
+
const options: MouseClickOptions = {};
|
|
101
|
+
if (action.button && action.button !== 'left') options.button = action.button;
|
|
102
|
+
if (modifiers.length) options.modifiers = modifiers;
|
|
103
|
+
// Do not render clickCount === 2 for dblclick.
|
|
104
|
+
if (action.clickCount && action.clickCount > 2)
|
|
105
|
+
options.clickCount = action.clickCount;
|
|
106
|
+
if (action.position) options.position = action.position;
|
|
107
|
+
return options;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatOptions(value: any, hasArguments: boolean): string {
|
|
111
|
+
const keys = Object.keys(value).filter(key => value[key] !== undefined);
|
|
112
|
+
if (!keys.length) return '';
|
|
113
|
+
return (hasArguments ? ', ' : '') + formatObject(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function quote(text = ''): string {
|
|
117
|
+
return escapeWithQuotes(text, "'");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function quoteMultiline(text: string, indent = ' ') {
|
|
121
|
+
const escape = (t: string) =>
|
|
122
|
+
t.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
123
|
+
const lines = text.split('\n');
|
|
124
|
+
if (lines.length === 1) return '`' + escape(text) + '`';
|
|
125
|
+
return (
|
|
126
|
+
'`\n' +
|
|
127
|
+
lines
|
|
128
|
+
.map(line => indent + escape(line).replace(/\${/g, '\\${'))
|
|
129
|
+
.join('\n') +
|
|
130
|
+
`\n${indent}\``
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class JavaScriptLanguageGenerator {
|
|
135
|
+
protected _isTest: boolean;
|
|
136
|
+
|
|
137
|
+
constructor(isTest: boolean) {
|
|
138
|
+
this._isTest = isTest;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected _asLocator(selector: string) {
|
|
142
|
+
return asLocator('javascript', selector);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected _generateActionCall(
|
|
146
|
+
subject: string,
|
|
147
|
+
actionInContext: CodegenActionInContext
|
|
148
|
+
): string {
|
|
149
|
+
const action = actionInContext.action;
|
|
150
|
+
switch (action.name) {
|
|
151
|
+
case 'openPage':
|
|
152
|
+
throw Error('Not reached');
|
|
153
|
+
case 'closePage':
|
|
154
|
+
return `await ${subject}.close();`;
|
|
155
|
+
case 'click': {
|
|
156
|
+
let method = 'click';
|
|
157
|
+
if (action.clickCount === 2) method = 'dblclick';
|
|
158
|
+
const options = toClickOptionsForSourceCode(action);
|
|
159
|
+
const optionsString = formatOptions(options, false);
|
|
160
|
+
return `await ${subject}.${this._asLocator(
|
|
161
|
+
action.selector
|
|
162
|
+
)}.${method}(${optionsString});`;
|
|
163
|
+
}
|
|
164
|
+
case 'hover':
|
|
165
|
+
return `await ${subject}.${this._asLocator(
|
|
166
|
+
action.selector
|
|
167
|
+
)}.hover(${formatOptions({ position: action.position }, false)});`;
|
|
168
|
+
case 'check':
|
|
169
|
+
return `await ${subject}.${this._asLocator(action.selector)}.check();`;
|
|
170
|
+
case 'uncheck':
|
|
171
|
+
return `await ${subject}.${this._asLocator(
|
|
172
|
+
action.selector
|
|
173
|
+
)}.uncheck();`;
|
|
174
|
+
case 'fill':
|
|
175
|
+
return `await ${subject}.${this._asLocator(
|
|
176
|
+
action.selector
|
|
177
|
+
)}.fill(${quote(action.text)});`;
|
|
178
|
+
case 'setInputFiles': {
|
|
179
|
+
const files = action.files ?? [];
|
|
180
|
+
return `await ${subject}.${this._asLocator(
|
|
181
|
+
action.selector
|
|
182
|
+
)}.setInputFiles(${formatObject(
|
|
183
|
+
files.length === 1 ? files[0] : files
|
|
184
|
+
)});`;
|
|
185
|
+
}
|
|
186
|
+
case 'press': {
|
|
187
|
+
const modifiers = toKeyboardModifiers(action.modifiers);
|
|
188
|
+
const shortcut = [...modifiers, action.key].join('+');
|
|
189
|
+
return `await ${subject}.${this._asLocator(
|
|
190
|
+
action.selector
|
|
191
|
+
)}.press(${quote(shortcut)});`;
|
|
192
|
+
}
|
|
193
|
+
case 'navigate':
|
|
194
|
+
return `await ${subject}.goto(${quote(action.url)});`;
|
|
195
|
+
case 'select': {
|
|
196
|
+
const opts = action.options ?? [];
|
|
197
|
+
return `await ${subject}.${this._asLocator(
|
|
198
|
+
action.selector
|
|
199
|
+
)}.selectOption(${formatObject(opts.length === 1 ? opts[0] : opts)});`;
|
|
200
|
+
}
|
|
201
|
+
case 'assertText':
|
|
202
|
+
return `${
|
|
203
|
+
this._isTest ? '' : '// '
|
|
204
|
+
}await expect(${subject}.${this._asLocator(action.selector)}).${
|
|
205
|
+
action.substring ? 'toContainText' : 'toHaveText'
|
|
206
|
+
}(${quote(action.text)});`;
|
|
207
|
+
case 'assertChecked':
|
|
208
|
+
return `${
|
|
209
|
+
this._isTest ? '' : '// '
|
|
210
|
+
}await expect(${subject}.${this._asLocator(action.selector)})${
|
|
211
|
+
action.checked ? '' : '.not'
|
|
212
|
+
}.toBeChecked();`;
|
|
213
|
+
case 'assertVisible':
|
|
214
|
+
return `${
|
|
215
|
+
this._isTest ? '' : '// '
|
|
216
|
+
}await expect(${subject}.${this._asLocator(
|
|
217
|
+
action.selector
|
|
218
|
+
)}).toBeVisible();`;
|
|
219
|
+
case 'assertValue': {
|
|
220
|
+
const assertion = action.value
|
|
221
|
+
? `toHaveValue(${quote(action.value)})`
|
|
222
|
+
: `toBeEmpty()`;
|
|
223
|
+
return `${
|
|
224
|
+
this._isTest ? '' : '// '
|
|
225
|
+
}await expect(${subject}.${this._asLocator(
|
|
226
|
+
action.selector
|
|
227
|
+
)}).${assertion};`;
|
|
228
|
+
}
|
|
229
|
+
case 'assertSnapshot': {
|
|
230
|
+
const commentIfNeeded = this._isTest ? '' : '// ';
|
|
231
|
+
return `${commentIfNeeded}await expect(${subject}.${this._asLocator(
|
|
232
|
+
action.selector
|
|
233
|
+
)}).toMatchAriaSnapshot(${quoteMultiline(
|
|
234
|
+
action.ariaSnapshot ?? '',
|
|
235
|
+
`${commentIfNeeded} `
|
|
236
|
+
)});`;
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
return '';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class JavaScriptFormatter {
|
|
245
|
+
private _baseIndent: string;
|
|
246
|
+
private _baseOffset: string;
|
|
247
|
+
private _lines: string[] = [];
|
|
248
|
+
|
|
249
|
+
constructor(offset = 0) {
|
|
250
|
+
this._baseIndent = ' '.repeat(2);
|
|
251
|
+
this._baseOffset = ' '.repeat(offset);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
prepend(text: string) {
|
|
255
|
+
const trim = isMultilineString(text)
|
|
256
|
+
? (line: string) => line
|
|
257
|
+
: (line: string) => line.trim();
|
|
258
|
+
this._lines = text.trim().split('\n').map(trim).concat(this._lines);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
add(text: string) {
|
|
262
|
+
const trim = isMultilineString(text)
|
|
263
|
+
? (line: string) => line
|
|
264
|
+
: (line: string) => line.trim();
|
|
265
|
+
this._lines.push(...text.trim().split('\n').map(trim));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
newLine() {
|
|
269
|
+
this._lines.push('');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
format(): string {
|
|
273
|
+
let spaces = '';
|
|
274
|
+
let previousLine = '';
|
|
275
|
+
return this._lines
|
|
276
|
+
.map((line: string) => {
|
|
277
|
+
if (line === '') return line;
|
|
278
|
+
if (line.startsWith('}') || line.startsWith(']'))
|
|
279
|
+
spaces = spaces.substring(this._baseIndent.length);
|
|
280
|
+
|
|
281
|
+
const extraSpaces = /^(for|while|if|try).*\(.*\)$/.test(previousLine)
|
|
282
|
+
? this._baseIndent
|
|
283
|
+
: '';
|
|
284
|
+
previousLine = line;
|
|
285
|
+
|
|
286
|
+
const callCarryOver = line.startsWith('.set');
|
|
287
|
+
line =
|
|
288
|
+
spaces + extraSpaces + (callCarryOver ? this._baseIndent : '') + line;
|
|
289
|
+
if (line.endsWith('{') || line.endsWith('['))
|
|
290
|
+
spaces += this._baseIndent;
|
|
291
|
+
return this._baseOffset + line;
|
|
292
|
+
})
|
|
293
|
+
.join('\n');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isMultilineString(text: string) {
|
|
298
|
+
return text.match(/`[\S\s]*`/)?.[0].includes('\n');
|
|
299
|
+
}
|
|
@@ -23,10 +23,7 @@
|
|
|
23
23
|
*
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
JavaScriptLanguageGenerator,
|
|
28
|
-
JavaScriptFormatter,
|
|
29
|
-
} from 'playwright-core/lib/server/codegen/javascript';
|
|
26
|
+
import { JavaScriptLanguageGenerator, JavaScriptFormatter } from './codegen';
|
|
30
27
|
|
|
31
28
|
export type Step = {
|
|
32
29
|
actions: ActionInContext[];
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export async function run(options: RunOptions) {
|
|
|
35
35
|
*/
|
|
36
36
|
export {
|
|
37
37
|
journey,
|
|
38
|
+
apiJourney,
|
|
38
39
|
step,
|
|
39
40
|
monitor,
|
|
40
41
|
beforeAll,
|
|
@@ -82,6 +83,11 @@ export type {
|
|
|
82
83
|
JourneyWithAnnotations,
|
|
83
84
|
JourneyOptions,
|
|
84
85
|
StepWithAnnotations,
|
|
86
|
+
APIJourney,
|
|
87
|
+
APIJourneyCallback,
|
|
88
|
+
APIJourneyCallbackOpts,
|
|
89
|
+
APIJourneyOptions,
|
|
90
|
+
APIJourneyWithAnnotations,
|
|
85
91
|
} from './dsl';
|
|
86
92
|
|
|
87
93
|
export type {
|
package/src/loader.ts
CHANGED
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { stdin, cwd } from 'process';
|
|
27
|
-
import { extname, resolve } from 'path';
|
|
27
|
+
import { extname, resolve, join } from 'path';
|
|
28
|
+
import { tmpdir } from 'os';
|
|
29
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
30
|
+
import Module from 'module';
|
|
28
31
|
import { CliArgs } from './common_types';
|
|
29
32
|
import { step, journey } from './core';
|
|
30
33
|
import { log } from './core/logger';
|
|
@@ -83,7 +86,21 @@ export async function loadTestFiles(options: CliArgs, args: string[]) {
|
|
|
83
86
|
requireSuites(suites);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
/**
|
|
90
|
+
* A leading `import`/`export` opts the inline source into module loading;
|
|
91
|
+
* anything else is treated as legacy statements with injected locals.
|
|
92
|
+
* Keying off `import`/`export` (not a `journey(` regex) avoids false
|
|
93
|
+
* matches inside strings/comments that would strip the implicit injection.
|
|
94
|
+
*/
|
|
95
|
+
const isModuleInlineSource = (source: string): boolean => {
|
|
96
|
+
return /^\s*(?:import|export)\b/m.test(source);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const loadInlineScript = (source: string) => {
|
|
100
|
+
if (isModuleInlineSource(source)) {
|
|
101
|
+
loadInlineModule(source);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
87
104
|
const scriptFn = new Function(
|
|
88
105
|
'step',
|
|
89
106
|
'page',
|
|
@@ -109,6 +126,65 @@ const loadInlineScript = source => {
|
|
|
109
126
|
});
|
|
110
127
|
};
|
|
111
128
|
|
|
129
|
+
const SYNTHETICS_PKG_ROOT = resolve(__dirname, '..');
|
|
130
|
+
let syntheticsAliasInstalled = false;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Alias `@elastic/synthetics` (and subpaths) to this package so an inline
|
|
134
|
+
* module materialised in a temp dir — whose ancestors may lack a
|
|
135
|
+
* `node_modules` entry — resolves to the currently running code.
|
|
136
|
+
*/
|
|
137
|
+
const installSyntheticsRequireAlias = () => {
|
|
138
|
+
if (syntheticsAliasInstalled) return;
|
|
139
|
+
syntheticsAliasInstalled = true;
|
|
140
|
+
const PKG = '@elastic/synthetics';
|
|
141
|
+
const ModuleInternal = Module as unknown as {
|
|
142
|
+
_resolveFilename: (
|
|
143
|
+
request: string,
|
|
144
|
+
parent: NodeModule,
|
|
145
|
+
isMain?: boolean,
|
|
146
|
+
options?: Record<string, unknown>
|
|
147
|
+
) => string;
|
|
148
|
+
};
|
|
149
|
+
const originalResolve = ModuleInternal._resolveFilename;
|
|
150
|
+
ModuleInternal._resolveFilename = function (
|
|
151
|
+
request,
|
|
152
|
+
parent,
|
|
153
|
+
isMain,
|
|
154
|
+
options
|
|
155
|
+
) {
|
|
156
|
+
if (request === PKG || request.startsWith(`${PKG}/`)) {
|
|
157
|
+
const subpath = request === PKG ? '' : request.slice(PKG.length + 1);
|
|
158
|
+
const aliased = subpath
|
|
159
|
+
? join(SYNTHETICS_PKG_ROOT, subpath)
|
|
160
|
+
: SYNTHETICS_PKG_ROOT;
|
|
161
|
+
return originalResolve.call(this, aliased, parent, isMain, options);
|
|
162
|
+
}
|
|
163
|
+
return originalResolve.call(this, request, parent, isMain, options);
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Materialise a self-contained inline module to disk and `require` it. The
|
|
169
|
+
* esbuild transform hook rewrites ESM → CJS, so the script registers its
|
|
170
|
+
* journeys on import instead of being wrapped in an implicit journey.
|
|
171
|
+
*/
|
|
172
|
+
const loadInlineModule = (source: string) => {
|
|
173
|
+
installSyntheticsRequireAlias();
|
|
174
|
+
const dir = mkdtempSync(join(tmpdir(), 'synthetics-inline-'));
|
|
175
|
+
// Best-effort cleanup so long-lived hosts don't accumulate temp dirs.
|
|
176
|
+
process.once('exit', () => {
|
|
177
|
+
try {
|
|
178
|
+
rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
} catch {
|
|
180
|
+
/* best-effort */
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const file = join(dir, 'inline.journey.ts');
|
|
184
|
+
writeFileSync(file, source);
|
|
185
|
+
require(file);
|
|
186
|
+
};
|
|
187
|
+
|
|
112
188
|
/**
|
|
113
189
|
* Read the input from STDIN and run it as inline journey
|
|
114
190
|
*/
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2020-present, Elastic NV
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in
|
|
14
|
+
* all copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
* THE SOFTWARE.
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Request } from 'playwright-core';
|
|
27
|
+
import { NetworkInfo } from './common_types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resource Timing shape shared by `Request.timing()` and (>= 1.62)
|
|
31
|
+
* `APIResponse.timing()`. Values are milliseconds relative to `startTime`,
|
|
32
|
+
* `-1` when a phase did not happen or is unknown (e.g. reused keep-alive
|
|
33
|
+
* socket, HAR replay).
|
|
34
|
+
*/
|
|
35
|
+
export type ResourceTiming = ReturnType<Request['timing']>;
|
|
36
|
+
|
|
37
|
+
export const roundMilliSecs = (value: number): number => {
|
|
38
|
+
return Math.floor(value * 1000) / 1000;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* First positive number in the list, used to derive the `blocked` phase from
|
|
43
|
+
* whichever Resource Timing marker fired first.
|
|
44
|
+
*/
|
|
45
|
+
export const firstPositive = (numbers: number[]): number | null => {
|
|
46
|
+
for (let i = 0; i < numbers.length; ++i) {
|
|
47
|
+
if (numbers[i] > 0) {
|
|
48
|
+
return numbers[i];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maps a Playwright Resource Timing object into the per-phase `NetworkInfo`
|
|
56
|
+
* timings. `total` is left at `-1` for the caller to fill via `calcTotalTime`
|
|
57
|
+
* once `loadEndTime`/`requestSentTime` are known. `receive` resolves to `-1`
|
|
58
|
+
* until `responseEnd` is available (browser journeys learn it on
|
|
59
|
+
* `requestfinished`); API journeys have the full timing up front.
|
|
60
|
+
*/
|
|
61
|
+
export function getResourceTimings(
|
|
62
|
+
rtiming: ResourceTiming
|
|
63
|
+
): NetworkInfo['timings'] {
|
|
64
|
+
const blocked =
|
|
65
|
+
roundMilliSecs(
|
|
66
|
+
firstPositive([
|
|
67
|
+
rtiming.domainLookupStart,
|
|
68
|
+
rtiming.connectStart,
|
|
69
|
+
rtiming.requestStart,
|
|
70
|
+
])
|
|
71
|
+
) || -1;
|
|
72
|
+
const dns =
|
|
73
|
+
rtiming.domainLookupEnd !== -1
|
|
74
|
+
? roundMilliSecs(rtiming.domainLookupEnd - rtiming.domainLookupStart)
|
|
75
|
+
: -1;
|
|
76
|
+
const connect =
|
|
77
|
+
rtiming.connectEnd !== -1
|
|
78
|
+
? roundMilliSecs(rtiming.connectEnd - rtiming.connectStart)
|
|
79
|
+
: -1;
|
|
80
|
+
const ssl =
|
|
81
|
+
rtiming.secureConnectionStart !== -1
|
|
82
|
+
? roundMilliSecs(rtiming.connectEnd - rtiming.secureConnectionStart)
|
|
83
|
+
: -1;
|
|
84
|
+
const wait =
|
|
85
|
+
rtiming.responseStart !== -1
|
|
86
|
+
? roundMilliSecs(rtiming.responseStart - rtiming.requestStart)
|
|
87
|
+
: -1;
|
|
88
|
+
const receive =
|
|
89
|
+
rtiming.responseEnd !== -1
|
|
90
|
+
? roundMilliSecs(rtiming.responseEnd - rtiming.responseStart)
|
|
91
|
+
: -1;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
blocked,
|
|
95
|
+
dns,
|
|
96
|
+
ssl,
|
|
97
|
+
connect,
|
|
98
|
+
send: 0, // not exposed via the Resource Timing API
|
|
99
|
+
wait,
|
|
100
|
+
receive,
|
|
101
|
+
total: -1,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the `total` phase by summing the positive phases. Falls back to the
|
|
107
|
+
* wall-clock span between `requestSentTime` and `loadEndTime` when Resource
|
|
108
|
+
* Timing data is unavailable (`startTime <= 0`, e.g. HAR replay or aborted
|
|
109
|
+
* requests). Pure — the caller assigns the result onto `timings.total`.
|
|
110
|
+
*/
|
|
111
|
+
export function calcTotalTime(
|
|
112
|
+
entry: NetworkInfo,
|
|
113
|
+
rtiming: ResourceTiming
|
|
114
|
+
): number {
|
|
115
|
+
if (rtiming.startTime <= 0) {
|
|
116
|
+
const end =
|
|
117
|
+
entry.loadEndTime || entry.responseReceivedTime || entry.requestSentTime;
|
|
118
|
+
const total = roundMilliSecs((end - entry.requestSentTime) * 1000);
|
|
119
|
+
return total <= 0 ? -1 : total;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { timings } = entry;
|
|
123
|
+
return [
|
|
124
|
+
timings.blocked,
|
|
125
|
+
timings.dns,
|
|
126
|
+
timings.connect,
|
|
127
|
+
timings.wait,
|
|
128
|
+
timings.receive,
|
|
129
|
+
].reduce((pre, cur) => ((cur || -1) > 0 ? cur + pre : pre), 0);
|
|
130
|
+
}
|