@jahia/cypress 7.4.0 → 8.1.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +69 -2
  3. package/dist/index.js +6 -2
  4. package/dist/injections/bash-data.d.ts +1 -0
  5. package/dist/injections/bash-data.js +57 -0
  6. package/dist/injections/chars-data.d.ts +1 -0
  7. package/dist/injections/chars-data.js +25 -0
  8. package/dist/injections/htmlentities-data.d.ts +1 -0
  9. package/dist/injections/htmlentities-data.js +22 -0
  10. package/dist/injections/numbers-data.d.ts +1 -0
  11. package/dist/injections/numbers-data.js +66 -0
  12. package/dist/injections/sql-data.d.ts +1 -0
  13. package/dist/injections/sql-data.js +82 -0
  14. package/dist/injections/xss-data.d.ts +1 -0
  15. package/dist/injections/xss-data.js +740 -0
  16. package/dist/page-object/baseComponent.d.ts +1 -2
  17. package/dist/page-object/baseComponent.js +10 -6
  18. package/dist/page-object/basePage.js +1 -1
  19. package/dist/page-object/html/iframe.d.ts +0 -1
  20. package/dist/page-object/html/iframe.js +2 -2
  21. package/dist/page-object/html/index.js +6 -2
  22. package/dist/page-object/index.js +6 -2
  23. package/dist/page-object/material/index.js +6 -2
  24. package/dist/page-object/material/muiinput.d.ts +0 -1
  25. package/dist/page-object/material/muiinput.js +1 -1
  26. package/dist/page-object/material/muiradio.js +1 -1
  27. package/dist/page-object/moonstone/accordion.d.ts +0 -1
  28. package/dist/page-object/moonstone/accordion.js +2 -2
  29. package/dist/page-object/moonstone/button.js +1 -1
  30. package/dist/page-object/moonstone/collapsible.js +1 -1
  31. package/dist/page-object/moonstone/dropdown.js +2 -2
  32. package/dist/page-object/moonstone/index.js +6 -2
  33. package/dist/page-object/moonstone/menu.js +9 -9
  34. package/dist/page-object/moonstone/pagination.js +3 -3
  35. package/dist/page-object/moonstone/primaryNav.js +2 -2
  36. package/dist/page-object/moonstone/secondaryNav.js +1 -1
  37. package/dist/page-object/moonstone/table.d.ts +0 -1
  38. package/dist/page-object/moonstone/table.js +5 -5
  39. package/dist/page-object/utils.d.ts +0 -1
  40. package/dist/page-object/utils.js +11 -12
  41. package/dist/plugins/env.js +2 -2
  42. package/dist/plugins/index.js +6 -2
  43. package/dist/plugins/registerPlugins.js +2 -2
  44. package/dist/support/apollo/apollo.d.ts +5 -4
  45. package/dist/support/apollo/apollo.js +80 -18
  46. package/dist/support/apollo/apolloClient.d.ts +1 -2
  47. package/dist/support/apollo/apolloClient.js +7 -7
  48. package/dist/support/apollo/index.js +6 -2
  49. package/dist/support/apollo/links.d.ts +1 -1
  50. package/dist/support/apollo/links.js +5 -6
  51. package/dist/support/browserHelper.d.ts +10 -0
  52. package/dist/support/browserHelper.js +167 -0
  53. package/dist/support/commands.js +1 -1
  54. package/dist/support/fixture.d.ts +1 -1
  55. package/dist/support/fixture.js +11 -7
  56. package/dist/support/index.d.ts +3 -0
  57. package/dist/support/index.js +9 -2
  58. package/dist/support/jfaker.d.ts +60 -0
  59. package/dist/support/jfaker.js +241 -0
  60. package/dist/support/jsErrorsLogger.js +13 -9
  61. package/dist/support/login.d.ts +0 -1
  62. package/dist/support/login.js +2 -2
  63. package/dist/support/logout.d.ts +0 -1
  64. package/dist/support/logout.js +1 -1
  65. package/dist/support/modSince.d.ts +52 -0
  66. package/dist/support/modSince.js +180 -0
  67. package/dist/support/provisioning/executeGroovy.d.ts +1 -1
  68. package/dist/support/provisioning/executeGroovy.js +42 -3
  69. package/dist/support/provisioning/index.js +6 -2
  70. package/dist/support/provisioning/installConfig.d.ts +0 -1
  71. package/dist/support/provisioning/installConfig.js +3 -3
  72. package/dist/support/provisioning/installModule.d.ts +0 -1
  73. package/dist/support/provisioning/installModule.js +1 -1
  74. package/dist/support/provisioning/runProvisioningScript.d.ts +4 -5
  75. package/dist/support/provisioning/runProvisioningScript.js +86 -9
  76. package/dist/support/provisioning/uninstallModule.d.ts +0 -1
  77. package/dist/support/provisioning/uninstallModule.js +1 -1
  78. package/dist/support/registerSupport.js +35 -1
  79. package/dist/support/repeatUntil.d.ts +1 -2
  80. package/dist/support/repeatUntil.js +2 -2
  81. package/dist/support/testStep.js +2 -2
  82. package/dist/utils/ClusterHelper.js +1 -1
  83. package/dist/utils/ExportHelper.d.ts +2 -2
  84. package/dist/utils/ExportHelper.js +14 -10
  85. package/dist/utils/GraphQLHelper.js +21 -17
  86. package/dist/utils/JCRHelper.d.ts +1 -1
  87. package/dist/utils/JCRHelper.js +1 -1
  88. package/dist/utils/JahiaPlatformHelper.js +2 -2
  89. package/dist/utils/Logger.js +6 -6
  90. package/dist/utils/PublicationAndWorkflowHelper.js +3 -3
  91. package/dist/utils/SAMHelper.d.ts +1 -1
  92. package/dist/utils/SAMHelper.js +4 -4
  93. package/dist/utils/SiteHelper.js +2 -2
  94. package/dist/utils/UsersHelper.js +2 -2
  95. package/dist/utils/VanityUrlHelper.js +1 -1
  96. package/dist/utils/index.js +6 -2
  97. package/docs/browser-helper.md +158 -0
  98. package/docs/jfaker.md +450 -0
  99. package/package.json +13 -10
  100. package/src/injections/bash-data.ts +54 -0
  101. package/src/injections/chars-data.ts +22 -0
  102. package/src/injections/htmlentities-data.ts +19 -0
  103. package/src/injections/numbers-data.ts +63 -0
  104. package/src/injections/sql-data.ts +79 -0
  105. package/src/injections/xss-data.ts +737 -0
  106. package/src/page-object/baseComponent.ts +6 -6
  107. package/src/page-object/html/iframe.ts +3 -3
  108. package/src/page-object/material/muiinput.ts +1 -1
  109. package/src/page-object/material/muiradio.ts +1 -1
  110. package/src/page-object/moonstone/accordion.ts +1 -1
  111. package/src/page-object/moonstone/button.ts +1 -1
  112. package/src/page-object/moonstone/collapsible.ts +1 -1
  113. package/src/page-object/moonstone/dropdown.ts +1 -1
  114. package/src/page-object/moonstone/menu.ts +1 -1
  115. package/src/page-object/moonstone/pagination.ts +1 -1
  116. package/src/page-object/moonstone/primaryNav.ts +1 -1
  117. package/src/page-object/moonstone/secondaryNav.ts +1 -1
  118. package/src/page-object/moonstone/table.ts +2 -2
  119. package/src/support/apollo/apollo.ts +74 -11
  120. package/src/support/apollo/links.ts +1 -2
  121. package/src/support/browserHelper.ts +186 -0
  122. package/src/support/index.ts +3 -0
  123. package/src/support/jfaker.ts +245 -0
  124. package/src/support/modSince.ts +222 -0
  125. package/src/support/provisioning/executeGroovy.md +7 -1
  126. package/src/support/provisioning/executeGroovy.ts +46 -2
  127. package/src/support/provisioning/runProvisioningScript.ts +89 -12
  128. package/src/support/registerSupport.ts +29 -0
  129. package/tests/cypress/e2e/jfaker.spec.ts +411 -0
  130. package/tests/cypress/e2e/modSince.spec.ts +306 -0
  131. package/tests/cypress.config.ts +23 -0
  132. package/tests/package.json +41 -0
  133. package/tests/reporter-config.json +13 -0
  134. package/tests/yarn.lock +8578 -0
  135. package/tsconfig.json +3 -0
@@ -0,0 +1,245 @@
1
+ /* eslint-disable no-else-return */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ /**
4
+ * jfaker - A flexible data generator for Cypress tests, supporting both faker.js generated data and custom injection payloads.
5
+ *
6
+ * This module provides a unified interface to generate either faker data or custom injection payloads based on the method called and global settings.
7
+ * It uses a dynamic Proxy to handle method calls and determine whether to generate faker data or injection data.
8
+ *
9
+ * IMPORTANT:
10
+ * When using the generated strings from jfaker in Cypress commands like `.type()`, make sure to:
11
+ * use `parseSpecialCharSequences: false`, e.g.: `<input>.type(text, {parseSpecialCharSequences: false})`
12
+ * to prevent Cypress from interpreting special characters in the generated strings (e.g., {, }, [, ], etc.) as commands,
13
+ * which is especially important for injection payloads that may contain such characters.
14
+ */
15
+
16
+ import {faker} from '@faker-js/faker';
17
+
18
+ // Import injection data from corresponding files in injections-ts directory
19
+ import {xssData} from '../injections/xss-data';
20
+ import {sqlData} from '../injections/sql-data';
21
+ import {bashData} from '../injections/bash-data';
22
+ import {charsData} from '../injections/chars-data';
23
+ import {htmlentitiesData} from '../injections/htmlentities-data';
24
+ import {numbersData} from '../injections/numbers-data';
25
+
26
+ const injectionData: Record<string, string[]> = {
27
+ xss: xssData,
28
+ sql: sqlData,
29
+ bash: bashData,
30
+ chars: charsData,
31
+ htmlentities: htmlentitiesData,
32
+ numbers: numbersData
33
+ };
34
+
35
+ // Environment variable key for storing injection type in Cypress env
36
+ // Can be set either using corresponding FakeData method or as an env var from CI/CD pipeline
37
+ const ENV_INJECTIONS_TYPE = 'JAHIA_CYPRESS_INJECTION_TYPE';
38
+
39
+ // Default range for random length of injection payloads; used when length is undefined
40
+ // Random items within the range will be picked and joined into a single string
41
+ const injectionsDefaultLength = {min: 2, max: 5};
42
+
43
+ /**
44
+ * Store FakeData type in Cypress env for persistence across specs
45
+ * @param {string} type FakeData type: 'faker' | 'xss' | 'sql' | 'bash' | 'chars' | 'htmlentities' | 'numbers'
46
+ * @returns void
47
+ */
48
+ function setDataType(type: string): void {
49
+ Cypress.env(ENV_INJECTIONS_TYPE, type);
50
+ }
51
+
52
+ /**
53
+ * Retrieve FakeData type from Cypress env (defaults to 'faker' if not set)
54
+ * @returns {string} Type of FakeData to use
55
+ */
56
+ function getDataType(): string | undefined {
57
+ return Cypress.env(ENV_INJECTIONS_TYPE) || 'faker';
58
+ }
59
+
60
+ /**
61
+ * Generate injection data based on the specified type and length
62
+ * @param {string} type Injection type to generate (xss, sql, bash, chars, htmlentities, numbers)
63
+ * @param {number} length Length of the generated injection (optional)
64
+ * @returns {string} Generated injection string
65
+ */
66
+ function generateInjection(type: string, length?: number): string {
67
+ let result: string[] = [];
68
+
69
+ // Type is specified and exists in injectionData, use it to generate data
70
+ const data = injectionData[type];
71
+ if (!data || data.length === 0) {
72
+ throw new Error(`[jFaker EXCEPTION] No injection data found for type: ${type}.`);
73
+ }
74
+
75
+ if (length === -1) {
76
+ // If length is -1, use all available items from the data array
77
+ result = data;
78
+ } else if (length && length > 0) {
79
+ // If length is specified and greater than 0, pick random items until the combined length meets the requirement
80
+ while (result.join('').length < length) {
81
+ const randomIndex = Math.floor(Math.random() * data.length);
82
+ result.push(data[randomIndex]);
83
+ }
84
+
85
+ // Trim the combined string to the specified length
86
+ const combined = result.join('');
87
+ result = [combined.substring(0, length)];
88
+ } else {
89
+ // If length is not specified, pick a random number of items within the default range
90
+ const itemsCount = Math.floor(Math.random() * injectionsDefaultLength.max) + injectionsDefaultLength.min;
91
+ for (let i = 0; i < itemsCount; i++) {
92
+ const randomIndex = Math.floor(Math.random() * data.length);
93
+ result.push(data[randomIndex]);
94
+ }
95
+ }
96
+
97
+ return result.join('');
98
+ }
99
+
100
+ /**
101
+ * Generate faker data based on the specified entity and options
102
+ * @param {string} entity Faker entity to generate (e.g., "person.firstName")
103
+ * @param {Record<string, unknown> | string | number | boolean | undefined} options Options to pass to the faker method (optional)
104
+ * @returns {string} Generated faker string
105
+ */
106
+ function generateFake(entity: string, options?: Record<string, unknown> | string | number | boolean | undefined): string {
107
+ let generator: () => string;
108
+ const result: string[] = [];
109
+ const parts: string[] = entity.split('.');
110
+ let fakerMethod: any = faker;
111
+
112
+ for (const part of parts) {
113
+ if (fakerMethod && typeof fakerMethod[part] !== 'undefined') {
114
+ fakerMethod = fakerMethod[part];
115
+ } else {
116
+ throw new Error(`[jFaker EXCEPTION] Invalid faker method: ${entity}`);
117
+ }
118
+ }
119
+
120
+ if (typeof fakerMethod === 'function') {
121
+ generator = (options && Object.keys(options).length > 0) ? () => fakerMethod(options) : () => fakerMethod();
122
+ } else {
123
+ throw new Error(`[jFaker EXCEPTION] ${entity} is not a function`);
124
+ }
125
+
126
+ result.push(generator());
127
+
128
+ return result.join('');
129
+ }
130
+
131
+ /**
132
+ * Escape string to prevent issues when used in contexts like HTML or JavaScript
133
+ * @param str
134
+ */
135
+ function escape(str: string): string {
136
+ return JSON.stringify(str).slice(1, -1);
137
+ }
138
+
139
+ /**
140
+ * Simple DeepApi class to create a dynamic nested Proxy for generating fake data using faker or injection data based on method calls
141
+ */
142
+ class DeepApi {
143
+ constructor(private handler: (path: string, args: any[]) => any) {
144
+ return this.createProxy([]);
145
+ }
146
+
147
+ private createProxy(path: string[]): any {
148
+ return new Proxy(
149
+ // The target is a function, so the proxy itself is callable
150
+ function () {},
151
+ {
152
+ // Handle property access - go deeper
153
+ get: (target, prop) => {
154
+ return this.createProxy([...path, String(prop)]);
155
+ },
156
+
157
+ // Handle function call - execute handler
158
+ apply: (target, thisArg, args) => {
159
+ return this.handler(path.join('.'), args);
160
+ }
161
+ }
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Interface to Fake Data generator (using DeepApi proxy to handle dynamic method calls)
168
+ * @param {Record<string, unknown>} options Options for data generation (length for injections, faker options, safe flag), \
169
+ * e.g.: `{length: 100}` for injections to specify desired length of the generated string, \
170
+ * or `{provider: 'example.com'}` for faker to pass options to the faker method. \
171
+ * For faker data generation, an additional option `safe` can be set to `true` \
172
+ * to force faker generation regardless of global type settings \
173
+ * (useful for specific cases where faker data is needed even when global type is set to injection).
174
+ * @remarks
175
+ * Available injection methods:
176
+ * - `.xss()` - Generate XSS injection payloads
177
+ * - `.sql()` - Generate SQL injection payloads
178
+ * - `.bash()` - Generate Bash injection payloads
179
+ * - `.chars()` - Generate random special characters
180
+ * - `.htmlentities()` - Generate HTML entities
181
+ * - `.numbers()` - Generate random numbers entities and edge cases
182
+ * - or any faker.js method can also be called (e.g., `person.firstName()`, `internet.email()`)
183
+ *
184
+ * @returns {string} Generated data string based on the method called and options provided
185
+ *
186
+ * @see https://fakerjs.dev/api/ for available faker methods and options
187
+ * @example
188
+ * ```typescript
189
+ *
190
+ * // Generate faker data with entity.
191
+ * const name = jfaker.person.firstName();
192
+ *
193
+ * // Entity will always be generated using faker (safe: true)
194
+ * const name = jfaker.person.firstName({safe: true});
195
+ *
196
+ * // Generate faker data with options.
197
+ * const email = jfaker.internet.email({provider: 'example.com'});
198
+ *
199
+ * // Generate injection payloads (random between min and max items joined into a single string)..
200
+ * // Entity will always be generated using 'xss'.
201
+ * const xssName = jfaker.xss();
202
+ *
203
+ * // Generate injection payloads with specific length.
204
+ * // Entity will always be generated using 'xss'.
205
+ * const xssName = jfaker.xss({length: 100});
206
+ *
207
+ * // Use all SQL injections.
208
+ * // Entity will always be generated using 'sql'.
209
+ * const allSql = jfaker.sql({length: -1});
210
+ * ```
211
+ */
212
+ const jfaker = new DeepApi((path, args) => {
213
+ // Handle non-faker methods first (escape, setFakeDataType, getFakeDataType)
214
+ switch (path) {
215
+ case 'escape':
216
+ return escape(args[0]);
217
+ case 'setDataType':
218
+ return setDataType(args[0]);
219
+ case 'getDataType':
220
+ return getDataType();
221
+ case Object.prototype.hasOwnProperty.call(injectionData, path) ? path : 'default':
222
+ // If the path corresponds to a valid injection type, generate data using that type,
223
+ // or fallback to default case which treats the path as a faker entity.
224
+ // Keep (safe === true) logic in "default" case for a readability.
225
+ return generateInjection(path, args[0]?.length);
226
+ default: {
227
+ // For faker data generation, check if the generation should be persistent based on global settings and options passed.
228
+ const safe = args[0]?.safe === true;
229
+ // Delete the 'safe' property from options to avoid passing it to faker methods
230
+ delete args[0]?.safe;
231
+
232
+ // Here path represents a faker entity.
233
+ // Check desired data type and 'safe' option to determine whether to generate faker data or injection data.
234
+ // If global type is set to injection (to override faker data) but safe is explicitly set to true in options -
235
+ // generate faker data for this specific call regardless of global settings.
236
+ if (getDataType() === 'faker' || safe) {
237
+ return generateFake(path, args[0]);
238
+ } else {
239
+ return generateInjection(getDataType(), args[0]?.length);
240
+ }
241
+ }
242
+ }
243
+ }) as any;
244
+
245
+ export {jfaker};
@@ -0,0 +1,222 @@
1
+ import {compare, validate} from 'compare-versions';
2
+ // Intentionally keep explicit path to avoid edge case errors in runtime
3
+ import {getJahiaVersion} from '../utils/JahiaPlatformHelper';
4
+
5
+ /** Cypress environment variable key used to store the current Jahia version. */
6
+ export const JAHIA_VERSION_ENV_VAR = 'CYPRESS_JAHIA_VERSION';
7
+
8
+ declare global {
9
+ // eslint-disable-next-line @typescript-eslint/no-namespace
10
+ namespace Mocha {
11
+ interface TestFunction {
12
+ since(requiredVersion: string, title: string, fn?: Func): Test;
13
+ since(requiredVersion: string, title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test;
14
+ }
15
+
16
+ interface ExclusiveTestFunction {
17
+ since(requiredVersion: string, title: string, fn?: Func): Test;
18
+ since(requiredVersion: string, title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test;
19
+ }
20
+
21
+ interface PendingTestFunction {
22
+ since(requiredVersion: string, title: string, fn?: Func): Test;
23
+ since(requiredVersion: string, title: string, config: Cypress.TestConfigOverrides, fn?: Func): Test;
24
+ }
25
+
26
+ interface SuiteFunction {
27
+ since(requiredVersion: string, title: string, fn: (this: Suite) => void): Suite;
28
+ }
29
+
30
+ interface ExclusiveSuiteFunction {
31
+ since(requiredVersion: string, title: string, fn: (this: Suite) => void): Suite;
32
+ }
33
+
34
+ interface PendingSuiteFunction {
35
+ since(requiredVersion: string, title: string, fn: (this: Suite) => void): Suite;
36
+ }
37
+ }
38
+ }
39
+
40
+ // ─── Internal helpers ────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Returns `true` when `current` satisfies `>= required`.
44
+ * Treats missing, empty, or unparseable versions as unsupported.
45
+ * @param current - The running Jahia version read from `Cypress.env`.
46
+ * @param required - Minimum version the test or suite needs.
47
+ */
48
+ const isSupported = (current: string | undefined, required: string): boolean => {
49
+ if (!current?.trim()) {
50
+ return false;
51
+ }
52
+
53
+ try {
54
+ return compare(String(current), required, '>=');
55
+ } catch {
56
+ return false;
57
+ }
58
+ };
59
+
60
+ /**
61
+ * Validates `since(...)` arguments and throws a descriptive error on misuse.
62
+ * Detects the common mistake of swapping `requiredVersion` and `title`.
63
+ * @param version - Version string passed as the first argument.
64
+ * @param title - Title string passed as the second argument.
65
+ * @param scope - Label used in the error message (e.g. `"it.since"`).
66
+ */
67
+ const assertArgs = (version: string, title: string, scope: string): void => {
68
+ if (!validate(version)) {
69
+ const hint = validate(title) ? ' (arguments appear swapped)' : '';
70
+ throw new Error(`[${scope}] Invalid version: "${version}"${hint}.`);
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Builds a human-readable message explaining why a test or suite was skipped.
76
+ * @param scope - Label for the helper (e.g. `"it.since"` or `"describe.since"`).
77
+ * @param title - Original test or suite title.
78
+ * @param required - Minimum version the test or suite needs.
79
+ * @param current - The running Jahia version; `undefined` when not yet fetched.
80
+ */
81
+ const skipReason = (scope: string, title: string, required: string, current?: string): string =>
82
+ current ?
83
+ `[${scope}] Skipping "${title}" — ${JAHIA_VERSION_ENV_VAR}="${current}" < required ${required}.` :
84
+ `[${scope}] Skipping "${title}" — ${JAHIA_VERSION_ENV_VAR} is not set. Required: ${required}.`;
85
+
86
+ // ─── Public API ───────────────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Fetches the Jahia version from the GraphQL API, strips the `-SNAPSHOT` suffix,
90
+ * and caches the result in `Cypress.env(JAHIA_VERSION_ENV_VAR)`.
91
+ */
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ export const initializeVersionSupport = (): Cypress.Chainable<any> =>
94
+ getJahiaVersion().then(jahiaVersion => {
95
+ const version = jahiaVersion.release.replace('-SNAPSHOT', '');
96
+ Cypress.env(JAHIA_VERSION_ENV_VAR, version);
97
+ return version;
98
+ });
99
+
100
+ /**
101
+ * Attaches `.since()` to `it`, `it.only`, `it.skip`, `describe`, `describe.only`,
102
+ * and `describe.skip`. Safe to call multiple times — subsequent calls are no-ops.
103
+ */
104
+ export const registerVersionSupport = (): void => {
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ const mochaIt = globalThis.it as any;
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ const mochaDescribe = globalThis.describe as any;
109
+
110
+ if (!mochaIt) {
111
+ throw new Error('Unable to register version support because Mocha `it` is not available.');
112
+ }
113
+
114
+ if (!mochaDescribe) {
115
+ throw new Error('Unable to register version support because Mocha `describe` is not available.');
116
+ }
117
+
118
+ // Attach .since() to it / it.only / it.skip
119
+ for (const target of [mochaIt, mochaIt.only, mochaIt.skip]) {
120
+ if (typeof target.since === 'function') {
121
+ continue;
122
+ }
123
+
124
+ const isSkip = target === mochaIt.skip;
125
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
+ target.since = (version: string, title: string, configOrFn?: any, maybeFn?: any) => {
127
+ assertArgs(version, title, 'it.since');
128
+
129
+ if (isSkip) {
130
+ // It.skip.since: always skip unconditionally, preserve the title
131
+ return typeof configOrFn === 'function' || configOrFn === undefined ?
132
+ target(title, configOrFn) :
133
+ target(title, configOrFn, maybeFn);
134
+ }
135
+
136
+ const userFn = typeof configOrFn === 'function' ? configOrFn : maybeFn;
137
+ const wrappedFn = function (this: Mocha.Context) {
138
+ const current = Cypress.env(JAHIA_VERSION_ENV_VAR);
139
+ if (!isSupported(current, version)) {
140
+ console.warn(skipReason('it.since', title, version, current));
141
+ this.skip();
142
+ } else if (typeof userFn === 'function') {
143
+ return userFn.call(this);
144
+ }
145
+ };
146
+
147
+ return typeof configOrFn === 'object' && configOrFn !== null ?
148
+ target(title, configOrFn, wrappedFn) :
149
+ target(title, wrappedFn);
150
+ };
151
+ }
152
+
153
+ // Attach .since() to describe / describe.only / describe.skip
154
+ for (const target of [mochaDescribe, mochaDescribe.only, mochaDescribe.skip]) {
155
+ if (typeof target.since === 'function') {
156
+ continue;
157
+ }
158
+
159
+ const isSkip = target === mochaDescribe.skip;
160
+ target.since = (version: string, title: string, fn: (this: Mocha.Suite) => void) => {
161
+ assertArgs(version, title, 'describe.since');
162
+
163
+ if (isSkip) {
164
+ // Describe.skip.since: always skip unconditionally, preserve the title
165
+ return target(title, fn);
166
+ }
167
+
168
+ return target(title, function (this: Mocha.Suite) {
169
+ // Suite-level runtime check runs after the global before() has fetched the version
170
+ before(function (this: Mocha.Context) {
171
+ const current = Cypress.env(JAHIA_VERSION_ENV_VAR);
172
+ if (!isSupported(current, version)) {
173
+ console.warn(skipReason('describe.since', title, version, current));
174
+ this.skip();
175
+ }
176
+ });
177
+
178
+ fn.call(this);
179
+ });
180
+ };
181
+ }
182
+
183
+ // Compatibility shim: redirect accidental it.skip(version, title, fn) → it.skip.since(...)
184
+ const origItSkip = mochaIt.skip;
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ mochaIt.skip = Object.assign((title: string, configOrTitle?: any, maybeFn?: any) => {
187
+ if (validate(title) && typeof configOrTitle === 'string' && typeof maybeFn === 'function') {
188
+ return origItSkip.since(title, configOrTitle, maybeFn);
189
+ }
190
+
191
+ return typeof configOrTitle === 'function' || configOrTitle === undefined ?
192
+ origItSkip(title, configOrTitle) :
193
+ origItSkip(title, configOrTitle, maybeFn);
194
+ }, {since: origItSkip.since});
195
+
196
+ const origDescribeSkip = mochaDescribe.skip;
197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
+ mochaDescribe.skip = Object.assign((title: string, fnOrTitle?: any, maybeFn?: any) => {
199
+ if (validate(title) && typeof fnOrTitle === 'string' && typeof maybeFn === 'function') {
200
+ return origDescribeSkip.since(title, fnOrTitle, maybeFn);
201
+ }
202
+
203
+ return origDescribeSkip(title, fnOrTitle);
204
+ }, {since: origDescribeSkip.since});
205
+ };
206
+
207
+ /**
208
+ * Enables version-gated testing for the Cypress suite.
209
+ * Registers `it.since`, `describe.since` (and their `.only`/`.skip` variants),
210
+ * then fetches the running Jahia version in a root `before()` hook.
211
+ *
212
+ * @example
213
+ * it.since('8.2.0', 'works on 8.2+', () => { ... });
214
+ * describe.since('8.2.0', 'suite for 8.2+', () => { ... });
215
+ */
216
+ function enable(): void {
217
+ registerVersionSupport();
218
+ before(() => initializeVersionSupport());
219
+ }
220
+
221
+ /** Public API for Jahia version-gated testing. */
222
+ export const modSince = {enable};
@@ -51,4 +51,10 @@ cy.executeGroovy("script.groovy").then(result => { console.log(result) })
51
51
 
52
52
  ## Command Log
53
53
 
54
- No log
54
+ When clicking on `groovy` within the command log, the console outputs the following:
55
+
56
+ - **Script**: The name of the Groovy script file executed.
57
+ - **Replacements**: The replacement map applied to the script, if any.
58
+ - **Server**: The Jahia server URL targeted.
59
+ - **Duration**: Elapsed time from invocation to completion.
60
+ - **Result**: The first operation result returned by the provisioning API.
@@ -25,6 +25,41 @@ const serverDefaults = {
25
25
  };
26
26
 
27
27
  export const executeGroovy = function (scriptFile: string, replacements?: { [key: string]: string }, jahiaServer: JahiaServer = serverDefaults): void {
28
+ let result: any;
29
+ let duration: number;
30
+ let scriptContent: string;
31
+ const startTime = Date.now();
32
+
33
+ const replacementsLabel = replacements && Object.keys(replacements).length > 0 ?
34
+ ` — ${JSON.stringify(replacements)}` :
35
+ '';
36
+
37
+ const logger = Cypress.log({
38
+ autoEnd: false,
39
+ name: 'executeGroovy',
40
+ displayName: 'groovy',
41
+ message: `${scriptFile}${replacementsLabel}`,
42
+ consoleProps: () => ({
43
+ Script: scriptFile,
44
+ 'Script Content': scriptContent ?? '(loading...)',
45
+ Replacements: replacements ?? {},
46
+ Server: jahiaServer.url,
47
+ Duration: duration === undefined ? 'pending' : `${duration}ms`,
48
+ Result: result
49
+ })
50
+ });
51
+
52
+ cy.fixture(scriptFile, 'utf-8').then((content: string) => {
53
+ let processed = content;
54
+ if (replacements) {
55
+ Object.keys(replacements).forEach(k => {
56
+ processed = processed.replaceAll(k, replacements[k]);
57
+ });
58
+ }
59
+
60
+ scriptContent = processed;
61
+ });
62
+
28
63
  cy.runProvisioningScript({
29
64
  script: {
30
65
  fileContent: '- executeScript: "' + scriptFile + '"',
@@ -36,6 +71,15 @@ export const executeGroovy = function (scriptFile: string, replacements?: { [key
36
71
  type: 'text/plain',
37
72
  encoding: 'utf-8'
38
73
  }],
39
- jahiaServer
40
- }).then(r => r[0]);
74
+ jahiaServer,
75
+ options: {log: false}
76
+ }).then(r => {
77
+ result = (r as any)?.[0];
78
+ duration = Date.now() - startTime;
79
+ const hasFailed = typeof result === 'string' && result.includes('.failed');
80
+ const prefix = hasFailed ? '❌ ' : '✅ ';
81
+ logger.set('message', `${prefix}${scriptFile}${replacementsLabel}`);
82
+ logger?.end();
83
+ return result;
84
+ });
41
85
  };
@@ -82,16 +82,74 @@ const serverDefaults: JahiaServer = {
82
82
  };
83
83
 
84
84
  function isFormFile(script: FormFile | StringDictionary[]): script is FormFile {
85
- return Boolean((script as FormFile).fileContent || (script as FormFile).fileName);
85
+ return Boolean((script as FormFile)?.fileContent || (script as FormFile)?.fileName);
86
86
  }
87
87
 
88
- export const runProvisioningScript = ({
89
- script,
90
- files,
91
- jahiaServer = serverDefaults,
92
- options = {log: true},
93
- requestOptions = {}
94
- }: RunProvisioningScriptParams): void => {
88
+ function getScriptSummary(script: FormFile | StringDictionary[]): string {
89
+ if (isFormFile(script)) {
90
+ if (script.fileName) {
91
+ return script.fileName;
92
+ }
93
+
94
+ if (script.fileContent) {
95
+ // Parse first operation and its value from YAML list: "- operationName: value"
96
+ const yamlMatch = script.fileContent.match(/^\s*-\s+(\w+)\s*:\s*"?([^"\n]+)"?/m);
97
+ if (yamlMatch) {
98
+ return `${yamlMatch[1]}: ${yamlMatch[2].trim()}`;
99
+ }
100
+
101
+ // Parse first operation name from JSON array: [{"operationName": ...}]
102
+ try {
103
+ const parsed = JSON.parse(script.fileContent);
104
+ if (Array.isArray(parsed) && parsed.length > 0) {
105
+ const ops = parsed.map((op: Record<string, string>) => Object.keys(op)[0] ?? 'unknown');
106
+ return ops.length === 1 ? ops[0] : `[${ops.join(', ')}]`;
107
+ }
108
+ } catch {
109
+ // Not valid JSON, fall through
110
+ }
111
+ }
112
+
113
+ return 'inline script';
114
+ }
115
+
116
+ if (!script || script.length === 0) {
117
+ return 'empty script';
118
+ }
119
+
120
+ const ops = script.map(op => Object.keys(op)[0] ?? 'unknown');
121
+ return ops.length === 1 ? ops[0] : `[${ops.join(', ')}]`;
122
+ }
123
+
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
+ export const runProvisioningScript = (paramsOrScript: RunProvisioningScriptParams | FormFile | StringDictionary[], ...rest: any[]): void => {
126
+ // Backward-compatible: support old positional signature
127
+ // runProvisioningScript(script, files, jahiaServer, options, timeout)
128
+ let script: FormFile | StringDictionary[];
129
+ let files: FormFile[];
130
+ let jahiaServer: JahiaServer;
131
+ let options: Cypress.Loggable;
132
+ let requestOptions: Partial<Cypress.RequestOptions>;
133
+
134
+ const isLegacyCall = Array.isArray(paramsOrScript) ||
135
+ (paramsOrScript as FormFile).fileContent !== undefined ||
136
+ (paramsOrScript as FormFile).fileName !== undefined;
137
+
138
+ if (isLegacyCall) {
139
+ script = paramsOrScript as FormFile | StringDictionary[];
140
+ files = rest[0];
141
+ jahiaServer = rest[1] ?? serverDefaults;
142
+ options = rest[2] ?? {log: true};
143
+ requestOptions = {};
144
+ } else {
145
+ const params = paramsOrScript as RunProvisioningScriptParams;
146
+ script = params.script;
147
+ files = params.files;
148
+ jahiaServer = params.jahiaServer ?? serverDefaults;
149
+ options = params.options ?? {log: true};
150
+ requestOptions = params.requestOptions ?? {};
151
+ }
152
+
95
153
  const formData = new FormData();
96
154
 
97
155
  if (isFormFile(script)) {
@@ -115,18 +173,30 @@ export const runProvisioningScript = ({
115
173
  let result: any;
116
174
  let logger: Cypress.Log;
117
175
 
176
+ const scriptSummary = getScriptSummary(script);
177
+ const replacementsFromFiles = files
178
+ ?.filter(f => f.replacements && Object.keys(f.replacements).length > 0)
179
+ .map(f => `${f.fileName}: ${JSON.stringify(f.replacements)}`);
180
+
118
181
  if (options.log) {
119
182
  logger = Cypress.log({
120
183
  autoEnd: false,
121
184
  name: 'runProvisioningScript',
122
185
  displayName: 'provScript',
123
- message: `Run ${isFormFile(script) && script.fileName ? script.fileName : 'inline script'} towards server: ${jahiaServer.url}`,
186
+ message: `${scriptSummary} @ ${jahiaServer.url}`,
124
187
  consoleProps: () => {
125
188
  return {
126
189
  Script: script,
127
- Files: files,
128
- Response: response,
129
- Yielded: result
190
+ Operations: isFormFile(script) ?
191
+ undefined :
192
+ script?.map(op => `${Object.keys(op)[0]}: ${Object.values(op)[0]}`),
193
+ Files: files?.map(f => f.fileName ?? 'inline file') ?? [],
194
+ Replacements: replacementsFromFiles?.length > 0 ? replacementsFromFiles : undefined,
195
+ Server: jahiaServer.url,
196
+ 'HTTP Status': response ? `${response.status} ${response.statusText}` : 'pending',
197
+ Duration: response ? `${response.duration}ms` : 'pending',
198
+ Result: result,
199
+ Response: response
130
200
  };
131
201
  }
132
202
  });
@@ -161,6 +231,13 @@ export const runProvisioningScript = ({
161
231
  }
162
232
 
163
233
  logger?.end();
234
+ if (logger) {
235
+ const hasFailed = res.status !== 200 ||
236
+ (Array.isArray(result) && result.some((r: any) => typeof r === 'string' && r.includes('.failed'))); // eslint-disable-line @typescript-eslint/no-explicit-any
237
+ const prefix = hasFailed ? '❌ ' : '✅ ';
238
+ logger.set('message', `${prefix}${scriptSummary} @ ${jahiaServer.url}`);
239
+ }
240
+
164
241
  return result;
165
242
  });
166
243
  };