donobu 3.4.0 → 3.5.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 (206) hide show
  1. package/README.md +14 -15
  2. package/dist/apis/FlowsApi.d.ts.map +1 -1
  3. package/dist/apis/FlowsApi.js +1 -0
  4. package/dist/apis/FlowsApi.js.map +1 -1
  5. package/dist/assets/generated/version +1 -1
  6. package/dist/assets/openapi-schema.html +6 -0
  7. package/dist/assets/openapi-schema.yaml +4 -0
  8. package/dist/bindings/PageInteractionTracker.d.ts.map +1 -1
  9. package/dist/bindings/PageInteractionTracker.js +5 -4
  10. package/dist/bindings/PageInteractionTracker.js.map +1 -1
  11. package/dist/codegen/CodeGenerator.d.ts +35 -0
  12. package/dist/codegen/CodeGenerator.d.ts.map +1 -0
  13. package/dist/codegen/CodeGenerator.js +882 -0
  14. package/dist/codegen/CodeGenerator.js.map +1 -0
  15. package/dist/esm/apis/FlowsApi.d.ts.map +1 -1
  16. package/dist/esm/apis/FlowsApi.js +1 -0
  17. package/dist/esm/apis/FlowsApi.js.map +1 -1
  18. package/dist/esm/assets/generated/version +1 -1
  19. package/dist/esm/assets/openapi-schema.html +6 -0
  20. package/dist/esm/assets/openapi-schema.yaml +4 -0
  21. package/dist/esm/bindings/PageInteractionTracker.d.ts.map +1 -1
  22. package/dist/esm/bindings/PageInteractionTracker.js +5 -4
  23. package/dist/esm/bindings/PageInteractionTracker.js.map +1 -1
  24. package/dist/esm/codegen/CodeGenerator.d.ts +35 -0
  25. package/dist/esm/codegen/CodeGenerator.d.ts.map +1 -0
  26. package/dist/esm/codegen/CodeGenerator.js +882 -0
  27. package/dist/esm/codegen/CodeGenerator.js.map +1 -0
  28. package/dist/esm/init.js +0 -15
  29. package/dist/esm/init.js.map +1 -1
  30. package/dist/esm/lib/PageAi.d.ts +39 -14
  31. package/dist/esm/lib/PageAi.d.ts.map +1 -1
  32. package/dist/esm/lib/PageAi.js +50 -32
  33. package/dist/esm/lib/PageAi.js.map +1 -1
  34. package/dist/esm/lib/SmartSelector.d.ts +8 -0
  35. package/dist/esm/lib/SmartSelector.d.ts.map +1 -1
  36. package/dist/esm/lib/SmartSelector.js +2 -2
  37. package/dist/esm/lib/SmartSelector.js.map +1 -1
  38. package/dist/esm/lib/createDonobuExtendedPage.js +2 -2
  39. package/dist/esm/lib/createDonobuExtendedPage.js.map +1 -1
  40. package/dist/esm/lib/fixtures/gptClients.js +2 -2
  41. package/dist/esm/lib/fixtures/gptClients.js.map +1 -1
  42. package/dist/esm/lib/pageAi/cache.d.ts +19 -23
  43. package/dist/esm/lib/pageAi/cache.d.ts.map +1 -1
  44. package/dist/esm/lib/pageAi/cache.js +105 -44
  45. package/dist/esm/lib/pageAi/cache.js.map +1 -1
  46. package/dist/esm/lib/pageAi/cacheEntryBuilder.d.ts +1 -3
  47. package/dist/esm/lib/pageAi/cacheEntryBuilder.d.ts.map +1 -1
  48. package/dist/esm/lib/pageAi/cacheEntryBuilder.js +6 -1
  49. package/dist/esm/lib/pageAi/cacheEntryBuilder.js.map +1 -1
  50. package/dist/esm/lib/testExtension.d.ts.map +1 -1
  51. package/dist/esm/lib/testExtension.js +2 -1
  52. package/dist/esm/lib/testExtension.js.map +1 -1
  53. package/dist/esm/lib/utils/selfHealing.js +1 -0
  54. package/dist/esm/lib/utils/selfHealing.js.map +1 -1
  55. package/dist/esm/lib/utils/triageTestFailure.d.ts.map +1 -1
  56. package/dist/esm/lib/utils/triageTestFailure.js +3 -2
  57. package/dist/esm/lib/utils/triageTestFailure.js.map +1 -1
  58. package/dist/esm/main.d.ts +4 -14
  59. package/dist/esm/main.d.ts.map +1 -1
  60. package/dist/esm/main.js +50 -35
  61. package/dist/esm/main.js.map +1 -1
  62. package/dist/esm/managers/AdminApiController.js +1 -1
  63. package/dist/esm/managers/AdminApiController.js.map +1 -1
  64. package/dist/esm/managers/DonobuFlow.js +4 -4
  65. package/dist/esm/managers/DonobuFlow.js.map +1 -1
  66. package/dist/esm/managers/DonobuFlowsManager.d.ts +33 -3
  67. package/dist/esm/managers/DonobuFlowsManager.d.ts.map +1 -1
  68. package/dist/esm/managers/DonobuFlowsManager.js +91 -7
  69. package/dist/esm/managers/DonobuFlowsManager.js.map +1 -1
  70. package/dist/esm/managers/GptConfigsManager.js +2 -2
  71. package/dist/esm/managers/GptConfigsManager.js.map +1 -1
  72. package/dist/esm/managers/RequestContextHolder.d.ts +3 -2
  73. package/dist/esm/managers/RequestContextHolder.d.ts.map +1 -1
  74. package/dist/esm/managers/RequestContextHolder.js +6 -9
  75. package/dist/esm/managers/RequestContextHolder.js.map +1 -1
  76. package/dist/esm/managers/ToolManager.js +1 -1
  77. package/dist/esm/managers/ToolManager.js.map +1 -1
  78. package/dist/esm/models/CodeGenerationOptions.d.ts +4 -0
  79. package/dist/esm/models/CodeGenerationOptions.d.ts.map +1 -1
  80. package/dist/esm/models/CodeGenerationOptions.js +4 -0
  81. package/dist/esm/models/CodeGenerationOptions.js.map +1 -1
  82. package/dist/esm/persistence/flows/FlowsPersistence.d.ts +5 -1
  83. package/dist/esm/persistence/flows/FlowsPersistence.d.ts.map +1 -1
  84. package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.d.ts +2 -1
  85. package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.d.ts.map +1 -1
  86. package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.js +50 -1
  87. package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.js.map +1 -1
  88. package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.d.ts +2 -1
  89. package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.d.ts.map +1 -1
  90. package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.js +31 -1
  91. package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.js.map +1 -1
  92. package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts +2 -1
  93. package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts.map +1 -1
  94. package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.js +23 -1
  95. package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.js.map +1 -1
  96. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts +2 -1
  97. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts.map +1 -1
  98. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +7 -1
  99. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js.map +1 -1
  100. package/dist/esm/persistence/flows/FlowsPersistenceSupabase.d.ts +2 -1
  101. package/dist/esm/persistence/flows/FlowsPersistenceSupabase.d.ts.map +1 -1
  102. package/dist/esm/persistence/flows/FlowsPersistenceSupabase.js +6 -1
  103. package/dist/esm/persistence/flows/FlowsPersistenceSupabase.js.map +1 -1
  104. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.d.ts +2 -1
  105. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.d.ts.map +1 -1
  106. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +17 -1
  107. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js.map +1 -1
  108. package/dist/esm/utils/createTool.d.ts +2 -0
  109. package/dist/esm/utils/createTool.d.ts.map +1 -1
  110. package/dist/esm/utils/createTool.js +2 -0
  111. package/dist/esm/utils/createTool.js.map +1 -1
  112. package/dist/init.js +0 -15
  113. package/dist/init.js.map +1 -1
  114. package/dist/lib/PageAi.d.ts +39 -14
  115. package/dist/lib/PageAi.d.ts.map +1 -1
  116. package/dist/lib/PageAi.js +50 -32
  117. package/dist/lib/PageAi.js.map +1 -1
  118. package/dist/lib/SmartSelector.d.ts +8 -0
  119. package/dist/lib/SmartSelector.d.ts.map +1 -1
  120. package/dist/lib/SmartSelector.js +2 -2
  121. package/dist/lib/SmartSelector.js.map +1 -1
  122. package/dist/lib/createDonobuExtendedPage.js +2 -2
  123. package/dist/lib/createDonobuExtendedPage.js.map +1 -1
  124. package/dist/lib/fixtures/gptClients.js +2 -2
  125. package/dist/lib/fixtures/gptClients.js.map +1 -1
  126. package/dist/lib/pageAi/cache.d.ts +19 -23
  127. package/dist/lib/pageAi/cache.d.ts.map +1 -1
  128. package/dist/lib/pageAi/cache.js +105 -44
  129. package/dist/lib/pageAi/cache.js.map +1 -1
  130. package/dist/lib/pageAi/cacheEntryBuilder.d.ts +1 -3
  131. package/dist/lib/pageAi/cacheEntryBuilder.d.ts.map +1 -1
  132. package/dist/lib/pageAi/cacheEntryBuilder.js +6 -1
  133. package/dist/lib/pageAi/cacheEntryBuilder.js.map +1 -1
  134. package/dist/lib/testExtension.d.ts.map +1 -1
  135. package/dist/lib/testExtension.js +2 -1
  136. package/dist/lib/testExtension.js.map +1 -1
  137. package/dist/lib/utils/selfHealing.js +1 -0
  138. package/dist/lib/utils/selfHealing.js.map +1 -1
  139. package/dist/lib/utils/triageTestFailure.d.ts.map +1 -1
  140. package/dist/lib/utils/triageTestFailure.js +3 -2
  141. package/dist/lib/utils/triageTestFailure.js.map +1 -1
  142. package/dist/main.d.ts +4 -14
  143. package/dist/main.d.ts.map +1 -1
  144. package/dist/main.js +50 -35
  145. package/dist/main.js.map +1 -1
  146. package/dist/managers/AdminApiController.js +1 -1
  147. package/dist/managers/AdminApiController.js.map +1 -1
  148. package/dist/managers/DonobuFlow.js +4 -4
  149. package/dist/managers/DonobuFlow.js.map +1 -1
  150. package/dist/managers/DonobuFlowsManager.d.ts +33 -3
  151. package/dist/managers/DonobuFlowsManager.d.ts.map +1 -1
  152. package/dist/managers/DonobuFlowsManager.js +91 -7
  153. package/dist/managers/DonobuFlowsManager.js.map +1 -1
  154. package/dist/managers/GptConfigsManager.js +2 -2
  155. package/dist/managers/GptConfigsManager.js.map +1 -1
  156. package/dist/managers/RequestContextHolder.d.ts +3 -2
  157. package/dist/managers/RequestContextHolder.d.ts.map +1 -1
  158. package/dist/managers/RequestContextHolder.js +6 -9
  159. package/dist/managers/RequestContextHolder.js.map +1 -1
  160. package/dist/managers/ToolManager.js +1 -1
  161. package/dist/managers/ToolManager.js.map +1 -1
  162. package/dist/models/CodeGenerationOptions.d.ts +4 -0
  163. package/dist/models/CodeGenerationOptions.d.ts.map +1 -1
  164. package/dist/models/CodeGenerationOptions.js +4 -0
  165. package/dist/models/CodeGenerationOptions.js.map +1 -1
  166. package/dist/persistence/flows/FlowsPersistence.d.ts +5 -1
  167. package/dist/persistence/flows/FlowsPersistence.d.ts.map +1 -1
  168. package/dist/persistence/flows/FlowsPersistenceAwsS3.d.ts +2 -1
  169. package/dist/persistence/flows/FlowsPersistenceAwsS3.d.ts.map +1 -1
  170. package/dist/persistence/flows/FlowsPersistenceAwsS3.js +50 -1
  171. package/dist/persistence/flows/FlowsPersistenceAwsS3.js.map +1 -1
  172. package/dist/persistence/flows/FlowsPersistenceFilesystem.d.ts +2 -1
  173. package/dist/persistence/flows/FlowsPersistenceFilesystem.d.ts.map +1 -1
  174. package/dist/persistence/flows/FlowsPersistenceFilesystem.js +31 -1
  175. package/dist/persistence/flows/FlowsPersistenceFilesystem.js.map +1 -1
  176. package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts +2 -1
  177. package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts.map +1 -1
  178. package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.js +23 -1
  179. package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.js.map +1 -1
  180. package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts +2 -1
  181. package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts.map +1 -1
  182. package/dist/persistence/flows/FlowsPersistenceSqlite.js +7 -1
  183. package/dist/persistence/flows/FlowsPersistenceSqlite.js.map +1 -1
  184. package/dist/persistence/flows/FlowsPersistenceSupabase.d.ts +2 -1
  185. package/dist/persistence/flows/FlowsPersistenceSupabase.d.ts.map +1 -1
  186. package/dist/persistence/flows/FlowsPersistenceSupabase.js +6 -1
  187. package/dist/persistence/flows/FlowsPersistenceSupabase.js.map +1 -1
  188. package/dist/persistence/flows/FlowsPersistenceVolatile.d.ts +2 -1
  189. package/dist/persistence/flows/FlowsPersistenceVolatile.d.ts.map +1 -1
  190. package/dist/persistence/flows/FlowsPersistenceVolatile.js +17 -1
  191. package/dist/persistence/flows/FlowsPersistenceVolatile.js.map +1 -1
  192. package/dist/utils/createTool.d.ts +2 -0
  193. package/dist/utils/createTool.d.ts.map +1 -1
  194. package/dist/utils/createTool.js +2 -0
  195. package/dist/utils/createTool.js.map +1 -1
  196. package/package.json +4 -6
  197. package/dist/assets/icon.png +0 -0
  198. package/dist/esm/assets/icon.png +0 -0
  199. package/dist/esm/managers/CodeGenerator.d.ts +0 -116
  200. package/dist/esm/managers/CodeGenerator.d.ts.map +0 -1
  201. package/dist/esm/managers/CodeGenerator.js +0 -886
  202. package/dist/esm/managers/CodeGenerator.js.map +0 -1
  203. package/dist/managers/CodeGenerator.d.ts +0 -116
  204. package/dist/managers/CodeGenerator.d.ts.map +0 -1
  205. package/dist/managers/CodeGenerator.js +0 -886
  206. package/dist/managers/CodeGenerator.js.map +0 -1
@@ -1,886 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CodeGenerator = void 0;
37
- const fs = __importStar(require("fs"));
38
- const cacheEntryBuilder_1 = require("../lib/pageAi/cacheEntryBuilder");
39
- const AnalyzePageTextTool_1 = require("../tools/AnalyzePageTextTool");
40
- const AssertPageTool_1 = require("../tools/AssertPageTool");
41
- const AssertTool_1 = require("../tools/AssertTool");
42
- const ChangeWebBrowserTabTool_1 = require("../tools/ChangeWebBrowserTabTool");
43
- const ChooseSelectOptionTool_1 = require("../tools/ChooseSelectOptionTool");
44
- const ClickTool_1 = require("../tools/ClickTool");
45
- const CreateBrowserCookieReportTool_1 = require("../tools/CreateBrowserCookieReportTool");
46
- const GoForwardOrBackTool_1 = require("../tools/GoForwardOrBackTool");
47
- const GoToWebpageTool_1 = require("../tools/GoToWebpageTool");
48
- const HoverOverElementTool_1 = require("../tools/HoverOverElementTool");
49
- const InputRandomizedEmailAddressTool_1 = require("../tools/InputRandomizedEmailAddressTool");
50
- const InputTextTool_1 = require("../tools/InputTextTool");
51
- const MakeCommentTool_1 = require("../tools/MakeCommentTool");
52
- const MarkObjectiveCompleteTool_1 = require("../tools/MarkObjectiveCompleteTool");
53
- const MarkObjectiveNotCompletableTool_1 = require("../tools/MarkObjectiveNotCompletableTool");
54
- const PressKeyTool_1 = require("../tools/PressKeyTool");
55
- const ReloadPageTool_1 = require("../tools/ReloadPageTool");
56
- const RememberPageTextTool_1 = require("../tools/RememberPageTextTool");
57
- const ReplayableInteraction_1 = require("../tools/ReplayableInteraction");
58
- const RunAccessibilityTestTool_1 = require("../tools/RunAccessibilityTestTool");
59
- const ScrollPageTool_1 = require("../tools/ScrollPageTool");
60
- const SummarizeLearningsTool_1 = require("../tools/SummarizeLearningsTool");
61
- const WaitTool_1 = require("../tools/WaitTool");
62
- const JsonUtils_1 = require("../utils/JsonUtils");
63
- const Logger_1 = require("../utils/Logger");
64
- const FlowDependencyAnalyzer_1 = require("./FlowDependencyAnalyzer");
65
- const ToolManager_1 = require("./ToolManager");
66
- function getLocalPlaywrightVersion() {
67
- const pkgPath = require.resolve('playwright/package.json');
68
- const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
69
- return pkgJson.version;
70
- }
71
- const PLAYWRIGHT_VERSION = getLocalPlaywrightVersion();
72
- class CodeGenerator {
73
- /** Creates a Node.js Microsoft Playwright script to replay the given flow. */
74
- async getFlowAsPlaywrightScript(flowMetadata, toolCalls) {
75
- // These tools are not supported in the generated script because they have
76
- // static outputs that have no side effects, so they are not actually
77
- // doing anything.
78
- const unsupportedToolsByName = new Set([
79
- MakeCommentTool_1.MakeCommentTool.NAME,
80
- MarkObjectiveCompleteTool_1.MarkObjectiveCompleteTool.NAME,
81
- MarkObjectiveNotCompletableTool_1.MarkObjectiveNotCompletableTool.NAME,
82
- SummarizeLearningsTool_1.SummarizeLearningsTool.NAME,
83
- ]);
84
- const envVarsAnnotations = (flowMetadata.envVars ?? []).map((env) => {
85
- return `{
86
- type: 'ENV',
87
- description: '${env}'
88
- }`;
89
- });
90
- const allTools = await ToolManager_1.ToolManager.allTools();
91
- const isGptClientRequired = this.isGptClientRequired(flowMetadata, toolCalls, allTools);
92
- const gptSetupNote = isGptClientRequired
93
- ? `* Note that this test uses tools that require the usage of an LLM, so be
94
- * sure to have an appropriate LLM API key available. This can be done
95
- * by providing an environment variable (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY,
96
- * or GOOGLE_GENERATIVE_AI_API_KEY) when running the test...
97
- *
98
- * Example: \`OPENAI_API_KEY=YOUR_KEY npx playwright test\`
99
- *
100
- * ...or by configuring a flow runner using the Donobu app.
101
- `
102
- : ' ';
103
- const hasObjective = (flowMetadata.overallObjective?.trim().length ?? 0) > 0;
104
- const testDetails = hasObjective
105
- ? `{
106
- annotation: [
107
- {
108
- type: 'objective',
109
- description: \`${this.sanitizeForTemplateLiteral(flowMetadata.overallObjective ?? '')}\`
110
- },
111
- ${envVarsAnnotations.join(',')}
112
- ]
113
- }`
114
- : '{}';
115
- const testName = flowMetadata.name
116
- ? flowMetadata.name
117
- // Escape backslashes first.
118
- .replace(/\\/g, '\\\\')
119
- // Escape single quotes.
120
- .replace(/'/g, "\\'")
121
- // Escape newlines.
122
- .replace(/\n/g, '\\n')
123
- // Escape carriage returns.
124
- .replace(/\r/g, '\\r')
125
- : `Test for ${flowMetadata.targetWebsite}`;
126
- const useStorageState = flowMetadata.browser && flowMetadata.browser.initialState
127
- ? `storageState: getBrowserStorageStateFixture(${JSON.stringify(flowMetadata.browser.initialState, null, 2)}),`
128
- : undefined;
129
- let testExtension = '';
130
- if (useStorageState) {
131
- testExtension = `.extend({${useStorageState}})`;
132
- }
133
- const scriptedToolCalls = toolCalls
134
- .filter((toolCall) => !unsupportedToolsByName.has(toolCall.name))
135
- .map((toolCall) => {
136
- return this.convertProposedToolCallToPlaywrightCode(toolCall);
137
- })
138
- .join('\n');
139
- const resultJson = flowMetadata.resultJsonSchema
140
- ? `// Extract an object from the page using the following JSON-schema.
141
- const extractedObject = await page.ai.extract(
142
- jsonSchemaToZod(${JSON.stringify(flowMetadata.resultJsonSchema)})
143
- );
144
- testInfo.attach('extracted-object', { body: JSON.stringify(extractedObject), contentType: 'application/json' });`
145
- : '';
146
- const needsExpectImport = toolCalls.some((toolCall) => toolCall.name === AssertPageTool_1.AssertPageTool.NAME);
147
- const needsJsonSchemaToZodImport = flowMetadata.resultJsonSchema;
148
- const preamble = gptSetupNote.trim().length > 0
149
- ? `/**
150
- ${gptSetupNote}*/`
151
- : '';
152
- const script = `${preamble}
153
- import { test${needsExpectImport ? ', expect' : ''}${needsJsonSchemaToZodImport ? ', jsonSchemaToZod' : ''} } from 'donobu';${useStorageState
154
- ? "\nimport { getBrowserStorageStateFixture } from 'donobu';"
155
- : ''}
156
-
157
- const title = '${testName}';
158
- const details = ${testDetails};
159
- test${testExtension}(title, details, async ({ page }${flowMetadata.resultJsonSchema ? ', testInfo' : ''}) => {
160
- ${scriptedToolCalls}
161
- ${resultJson}
162
- });
163
- `;
164
- return this.prettifyCode(script);
165
- }
166
- /** Creates a Node.js Microsoft Playwright script to replay the given flow. */
167
- async getFlowAsAiPlaywrightScript(flowMetadata, toolCalls) {
168
- const gptSetupNote = ` * This test replays a recorded Donobu flow via \`page.ai.act(...)\` using the cached
169
- * tool calls stored alongside this spec in \`donobu.json\`. If the cache entry
170
- * is missing or the parameters change, the run falls back to autonomous mode
171
- * and will require a GPT API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, or
172
- * GOOGLE_GENERATIVE_AI_API_KEY).
173
- `;
174
- const testName = flowMetadata.name
175
- ? flowMetadata.name
176
- // Escape backslashes first.
177
- .replace(/\\/g, '\\\\')
178
- // Escape single quotes.
179
- .replace(/'/g, "\\'")
180
- // Escape newlines.
181
- .replace(/\n/g, '\\n')
182
- // Escape carriage returns.
183
- .replace(/\r/g, '\\r')
184
- : `Test for ${flowMetadata.targetWebsite}`;
185
- const useStorageState = flowMetadata.browser && flowMetadata.browser.initialState
186
- ? `storageState: getBrowserStorageStateFixture(${JSON.stringify(flowMetadata.browser.initialState, null, 2)}),`
187
- : undefined;
188
- let testExtension = '';
189
- if (useStorageState) {
190
- testExtension = `.extend({${useStorageState}})`;
191
- }
192
- const instructionSource = flowMetadata.overallObjective &&
193
- flowMetadata.overallObjective.trim().length > 0
194
- ? flowMetadata.overallObjective
195
- : `Replay the recorded flow for ${flowMetadata.targetWebsite}`;
196
- const sanitizedInstruction = this.sanitizeForTemplateLiteral(instructionSource);
197
- const defaultToolNames = new Set((await ToolManager_1.ToolManager.defaultTools()).map((tool) => tool.name));
198
- const toolNamesFromCalls = Array.from(new Set(toolCalls.map((toolCall) => toolCall.name))).sort();
199
- const usesNonDefaultTool = toolNamesFromCalls.some((toolName) => !defaultToolNames.has(toolName));
200
- const optionsLines = [];
201
- const needsJsonSchemaToZodImport = flowMetadata.resultJsonSchema !== null;
202
- if (flowMetadata.resultJsonSchema) {
203
- optionsLines.push(`schema: jsonSchemaToZod(${JSON.stringify(flowMetadata.resultJsonSchema)})`);
204
- }
205
- if (usesNonDefaultTool && toolNamesFromCalls.length > 0) {
206
- optionsLines.push(`allowedTools: ${JSON.stringify(toolNamesFromCalls)}`);
207
- }
208
- if (flowMetadata.maxToolCalls !== null) {
209
- optionsLines.push(`maxToolCalls: ${flowMetadata.maxToolCalls}`);
210
- }
211
- if (flowMetadata.envVars && flowMetadata.envVars.length > 0) {
212
- optionsLines.push(`envVars: ${JSON.stringify(flowMetadata.envVars)}`);
213
- }
214
- const aiOptionsLiteral = optionsLines.length > 0
215
- ? `{\n${optionsLines.map((line) => ` ${line}`).join(',\n')}\n }`
216
- : '';
217
- const aiCallExpression = optionsLines.length > 0
218
- ? `page.ai.act(\`${sanitizedInstruction}\`, ${aiOptionsLiteral})`
219
- : `page.ai.act(\`${sanitizedInstruction}\`)`;
220
- const needsTestInfo = flowMetadata.resultJsonSchema !== null;
221
- const cacheComment = ' // Cached tool calls for this flow live in donobu.json next to this spec.\n';
222
- const aiInvocation = needsTestInfo
223
- ? ` const aiResult = await ${aiCallExpression};
224
- await testInfo.attach('extracted-object', { body: JSON.stringify(aiResult, null, 2), contentType: 'application/json' });
225
- `
226
- : ` await ${aiCallExpression};
227
- `;
228
- const preamble = `/**
229
- ${gptSetupNote}*/`;
230
- const script = `${preamble}
231
- import { test${needsJsonSchemaToZodImport ? ', jsonSchemaToZod' : ''} } from 'donobu';${useStorageState ? "\nimport { getBrowserStorageStateFixture } from 'donobu';" : ''}
232
-
233
- test${testExtension}('${testName}', async ({ page }${needsTestInfo ? ', testInfo' : ''}) => {
234
- ${cacheComment}${aiInvocation}});
235
- `;
236
- return this.prettifyCode(script);
237
- }
238
- /**
239
- * Generates a complete Playwright project structure with dependency management.
240
- */
241
- async generateProject(flowsWithToolCalls, gptConfig, options) {
242
- // Analyze dependencies
243
- const flowsMetadata = flowsWithToolCalls.map((f) => f.metadata);
244
- const dependencyGraph = FlowDependencyAnalyzer_1.FlowDependencyAnalyzer.analyzeDependencies(flowsMetadata);
245
- const files = [];
246
- // Generate individual test files
247
- const testFiles = await this.generateTestFiles(flowsWithToolCalls);
248
- files.push(...testFiles);
249
- // Generate playwright.config.ts
250
- files.push({
251
- path: 'playwright.config.ts',
252
- content: await this.generatePlaywrightConfig(dependencyGraph, options),
253
- });
254
- files.push({
255
- path: '.github/workflows/run-tests.yml',
256
- content: await this.generateGitHubActionsWorkflow(flowsWithToolCalls, gptConfig, options),
257
- });
258
- // Generate package.json if needed
259
- files.push({
260
- path: 'package.json',
261
- content: this.generatePackageJson(),
262
- });
263
- // Generate README
264
- files.push({
265
- path: 'README.md',
266
- content: this.generateReadme(),
267
- });
268
- files.push({
269
- path: '.gitignore',
270
- content: this.generateGitIgnore(),
271
- });
272
- const cacheFile = this.buildCacheFile(flowsWithToolCalls);
273
- if (cacheFile) {
274
- files.push(cacheFile);
275
- }
276
- return { files };
277
- }
278
- async generateGitHubActionsWorkflow(flowsWithToolCalls, gptConfig, options) {
279
- const flowsMetadata = flowsWithToolCalls.map((f) => f.metadata);
280
- const allUniqueEnvVars = [
281
- ...new Set(flowsMetadata.flatMap((flow) => flow.envVars || [])),
282
- ];
283
- const allTools = await ToolManager_1.ToolManager.allTools();
284
- const isGptClientRequired = flowsWithToolCalls.some((f) => {
285
- return this.isGptClientRequired(f.metadata, f.toolCalls, allTools);
286
- }) || !options.disableSelfHealingTests;
287
- const envVarsList = [];
288
- if (!options.disableSelfHealingTests) {
289
- envVarsList.push('SELF_HEAL_TESTS_ENABLED: true');
290
- }
291
- if (isGptClientRequired) {
292
- const defaultGptSetup = [
293
- '# Uncomment the desired GPT provider and set up the secret in GitHub.',
294
- '# ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}',
295
- '# GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}',
296
- '# OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}',
297
- ];
298
- if (gptConfig) {
299
- switch (gptConfig.type) {
300
- case 'ANTHROPIC':
301
- envVarsList.push('ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}', `ANTHROPIC_MODEL_NAME: "${gptConfig.modelName}"`);
302
- break;
303
- case 'ANTHROPIC_AWS_BEDROCK':
304
- envVarsList.push('AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}', `AWS_ACCESS_KEY_ID: ${gptConfig.accessKeyId}`, `AWS_REGION: ${gptConfig.region}`, `AWS_BEDROCK_MODEL_NAME: "${gptConfig.modelName}"`);
305
- break;
306
- case 'DONOBU':
307
- envVarsList.push('DONOBU_API_KEY: ${{ secrets.DONOBU_API_KEY }}');
308
- break;
309
- case 'GOOGLE_GEMINI':
310
- envVarsList.push('GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}', `GOOGLE_GENERATIVE_AI_MODEL_NAME: "${gptConfig.modelName}"`);
311
- break;
312
- case 'OPENAI':
313
- envVarsList.push('OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}', `OPENAI_API_MODEL_NAME: "${gptConfig.modelName}"`);
314
- break;
315
- case 'OPENAI_AZURE':
316
- case 'LLAMA_LOCAL':
317
- case 'LLAMA_HUGGING_FACE':
318
- case 'VERCEL_AI':
319
- default:
320
- envVarsList.push(...defaultGptSetup);
321
- }
322
- }
323
- else {
324
- envVarsList.push(...defaultGptSetup);
325
- }
326
- }
327
- envVarsList.push(...allUniqueEnvVars.map((envVarName) => `${envVarName}: \${{ secrets.${envVarName} }}`));
328
- const envVarsSection = envVarsList.length > 0
329
- ? `\n env:\n ${envVarsList.join('\n ')}`
330
- : '';
331
- const triggerSection = `
332
- on:
333
- pull_request:
334
- workflow_dispatch:`;
335
- const xvfbStep = options.runInHeadedMode
336
- ? `
337
- - name: Install XVFB for headed mode
338
- run: sudo apt-get update && sudo apt-get install -y xvfb
339
- `
340
- : '';
341
- const testCommand = options.runInHeadedMode
342
- ? 'xvfb-run -a npx playwright test'
343
- : 'npx playwright test';
344
- const pullRequestCreationSection = options.disablePullRequestCreation
345
- ? ''
346
- : `
347
- # Create a self-healing PR only when this workflow was not triggered by a pull-request.
348
- - name: Automatically create a pull request for fixing failed tests (if any)
349
- if: \${{ github.event_name != 'pull_request' }}
350
- uses: peter-evans/create-pull-request@v5
351
- with:
352
- token: \${{ secrets.GITHUB_TOKEN }}
353
- commit-message: "Fix failing Playwright tests"
354
- title: "[Fixed] Playwright tests"
355
- body: "Fix failing Playwright tests"
356
- branch: fix-playwright-tests-for-\${{ github.ref_name }}
357
- base: \${{ github.ref_name }}`;
358
- return `name: Run Playwright Tests
359
- ${triggerSection}
360
-
361
- jobs:
362
- run-donobu-flows:
363
- runs-on: ubuntu-latest
364
-
365
- steps:
366
- - name: Checkout repository
367
- uses: actions/checkout@v4
368
-
369
- - name: Install dependencies
370
- run: npm install && npx playwright install --with-deps
371
- ${xvfbStep}
372
- - name: Run Playwright tests
373
- id: run_tests
374
- continue-on-error: true${envVarsSection}
375
- run: ${testCommand}
376
-
377
- - name: Upload Raw Results
378
- uses: actions/upload-artifact@v4
379
- if: always()
380
- with:
381
- name: test-results
382
- path: test-results/
383
- retention-days: 3
384
-
385
- - name: Upload HTML Report
386
- uses: actions/upload-artifact@v4
387
- if: always()
388
- with:
389
- name: html-report
390
- path: playwright-report/
391
- retention-days: 3
392
-
393
- - name: Report test results
394
- if: always()
395
- run: npm exec playwright-json-to-markdown < "test-results/playwright-report.json" >> $GITHUB_STEP_SUMMARY
396
-
397
- - name: Post to Slack
398
- if: always()
399
- env:
400
- SLACK_WEBHOOK_URL: \${{ secrets.SLACK_WEBHOOK_URL }}
401
- run: |
402
- if [ -n "$SLACK_WEBHOOK_URL" ]; then
403
- WORKFLOW_URL="\${GITHUB_SERVER_URL}/\${GITHUB_REPOSITORY}/actions/runs/\${GITHUB_RUN_ID}"
404
- SLACK_PAYLOAD=$(npm exec playwright-json-to-slack-json -- --report-url "$WORKFLOW_URL" < "test-results/playwright-report.json")
405
- curl --header 'Content-type: application/json' --data "$SLACK_PAYLOAD" "$SLACK_WEBHOOK_URL"
406
- else
407
- echo "SLACK_WEBHOOK_URL secret not present, skipping Slack notification."
408
- fi
409
- ${pullRequestCreationSection}`;
410
- }
411
- buildCacheContents(flowsWithToolCalls) {
412
- const entries = flowsWithToolCalls.map(({ metadata, toolCalls }) => {
413
- const cacheEntry = cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(metadata.targetWebsite, metadata, toolCalls);
414
- return {
415
- ...cacheEntry,
416
- allowedTools: [...cacheEntry.allowedTools],
417
- toolCallCache: JSON.parse(JSON.stringify(cacheEntry.toolCallCache)),
418
- schema: cacheEntry.schema === null
419
- ? null
420
- : JSON.parse(JSON.stringify(cacheEntry.schema)),
421
- };
422
- });
423
- return {
424
- caches: entries,
425
- };
426
- }
427
- buildCacheFile(flowsWithToolCalls) {
428
- if (flowsWithToolCalls.length === 0) {
429
- return null;
430
- }
431
- const contents = this.buildCacheContents(flowsWithToolCalls);
432
- return {
433
- path: 'donobu.json',
434
- content: JSON.stringify(contents, null, 2),
435
- };
436
- }
437
- /**
438
- * Maps a proposed Donobu tool call to valid NodeJS Playwright code that uses
439
- * the `DonobuExtendedPage` extension.
440
- */
441
- convertProposedToolCallToPlaywrightCode(proposedToolCall) {
442
- const rawParams = JsonUtils_1.JsonUtils.objectToJson(proposedToolCall.parameters);
443
- const rationale = rawParams.rationale && rawParams.rationale.trim().length > 0
444
- ? rawParams.rationale
445
- .split('\n')
446
- .map((line) => `// ${line}`.trim())
447
- .join('\n') + '\n'
448
- : '';
449
- // Delete fields that should not be directly mapped.
450
- delete rawParams.rationale;
451
- delete rawParams.whyThisAnnotation;
452
- delete rawParams.annotation;
453
- const hasNonEmptyParameters = Object.keys(rawParams).length > 0;
454
- const serializedParams = JSON.stringify(rawParams, null, 2);
455
- switch (proposedToolCall.name) {
456
- case AssertTool_1.AssertTool.NAME: {
457
- const options = {
458
- ...(rawParams.retries > 0 ? { retries: rawParams.retries } : {}),
459
- ...(rawParams.retries > 0 && rawParams.retryWaitSeconds > 0
460
- ? { retryWaitSeconds: rawParams.retryWaitSeconds }
461
- : {}),
462
- };
463
- const serializedOptions = Object.keys(options).length > 0
464
- ? `,${JSON.stringify(options, null, 2)}`
465
- : '';
466
- return `${rationale}await page.ai.assert(
467
- ${JSON.stringify(rawParams.assertionToTestFor)}${serializedOptions});`;
468
- }
469
- case ChangeWebBrowserTabTool_1.ChangeWebBrowserTabTool.NAME: {
470
- const url = ChangeWebBrowserTabTool_1.ChangeWebBrowserTabCoreSchema.parse(proposedToolCall.parameters);
471
- return `${rationale}page = await page.changeTab(${JSON.stringify(url.tabUrl)});`;
472
- }
473
- case ChooseSelectOptionTool_1.ChooseSelectOptionTool.NAME: {
474
- const find = this.parseFindCall(rawParams);
475
- const { selector: _selector, optionValues } = rawParams;
476
- return `${rationale}${find}.selectOption(${JSON.stringify(optionValues)})`;
477
- }
478
- case ClickTool_1.ClickTool.NAME: {
479
- const find = this.parseFindCall(rawParams);
480
- return `${rationale}${find}.click()`;
481
- }
482
- case GoForwardOrBackTool_1.GoForwardOrBackTool.NAME: {
483
- if (rawParams.direction === 'FORWARD') {
484
- return `${rationale}await page.goForward();`;
485
- }
486
- else if (rawParams.direction === 'BACK') {
487
- return `${rationale}await page.goBack();`;
488
- }
489
- else {
490
- throw new Error(`Invalid ${GoForwardOrBackTool_1.GoForwardOrBackTool.NAME} params: ${serializedParams}`);
491
- }
492
- }
493
- case GoToWebpageTool_1.GoToWebpageTool.NAME: {
494
- return `${rationale}await page.goto(${JSON.stringify(rawParams.url)});`;
495
- }
496
- case HoverOverElementTool_1.HoverOverElementTool.NAME: {
497
- const find = this.parseFindCall(rawParams);
498
- return `${rationale}${find}.hover()`;
499
- }
500
- case InputRandomizedEmailAddressTool_1.InputRandomizedEmailAddressTool.NAME: {
501
- const find = this.parseFindCall(rawParams);
502
- const { selector: _selector, baseEmail, finalizeWithSubmit, } = rawParams;
503
- return `${rationale}${find}.inputRandomizedEmailAddress(
504
- ${JSON.stringify(baseEmail)},
505
- ${finalizeWithSubmit ? JSON.stringify({ finalizeWithSubmit }) : ''})`;
506
- }
507
- case InputTextTool_1.InputTextTool.NAME: {
508
- const find = this.parseFindCall(rawParams);
509
- const { selector: _selector, text, append, finalizeWithSubmit, } = rawParams;
510
- return `${rationale}${find}.inputText(
511
- ${JSON.stringify(text)},
512
- ${append || finalizeWithSubmit ? JSON.stringify({ append, finalizeWithSubmit }) : ''})`;
513
- }
514
- case PressKeyTool_1.PressKeyTool.NAME: {
515
- const find = this.parseFindCall(rawParams);
516
- const { selector: _selector, key } = rawParams;
517
- return `${rationale}${find}.pressKey(${JSON.stringify(key)})`;
518
- }
519
- case ReloadPageTool_1.ReloadPageTool.NAME: {
520
- return `${rationale}await page.reload();`;
521
- }
522
- case ScrollPageTool_1.ScrollPageTool.NAME: {
523
- const find = this.parseFindCall(rawParams);
524
- const { selector: _selector, direction, maxScroll } = rawParams;
525
- return `${rationale}${find}.scroll(
526
- ${JSON.stringify(direction)},
527
- ${maxScroll ? JSON.stringify({ maxScroll }) : ''})`;
528
- }
529
- case WaitTool_1.WaitTool.NAME: {
530
- return `${rationale}await page.waitForTimeout(${rawParams.seconds * 1000});`;
531
- }
532
- case AssertPageTool_1.AssertPageTool.NAME: {
533
- const assertionType = rawParams.type;
534
- const expected = rawParams.expected;
535
- const isRegex = rawParams.isRegex || false;
536
- switch (assertionType) {
537
- case 'title': {
538
- if (isRegex) {
539
- return `${rationale}await expect(page).toHaveTitle(new RegExp(${JSON.stringify(expected)}));`;
540
- }
541
- else {
542
- return `${rationale}await expect(page).toHaveTitle('${expected}');`;
543
- }
544
- }
545
- case 'url': {
546
- if (isRegex) {
547
- return `${rationale}await expect(page).toHaveURL(new RegExp(${JSON.stringify(expected)}));`;
548
- }
549
- else {
550
- return `${rationale}await expect(page).toHaveURL('${expected}');`;
551
- }
552
- }
553
- case 'content': {
554
- if (isRegex) {
555
- return `${rationale}await expect(page.locator('body')).toContainText(new RegExp(${JSON.stringify(expected)}));`;
556
- }
557
- else {
558
- return `${rationale}await expect(page.getByText('${expected}')).toBeVisible();`;
559
- }
560
- }
561
- default: {
562
- // Fallback to the generic tool call if unknown type
563
- return `${rationale}await page.${proposedToolCall.name}(
564
- ${hasNonEmptyParameters ? JSON.stringify({ type: assertionType, expected, isRegex }, null, 2) : ''});`;
565
- }
566
- }
567
- }
568
- case RememberPageTextTool_1.RememberPageTextTool.NAME: {
569
- delete rawParams.text;
570
- const updatedSerializedParams = JSON.stringify(rawParams, null, 2);
571
- return `${rationale}await page.run('${proposedToolCall.name}', ${updatedSerializedParams});`;
572
- }
573
- case AnalyzePageTextTool_1.AnalyzePageTextTool.NAME: {
574
- return `${rationale}await page.ai.analyzePageText(
575
- ${JSON.stringify(rawParams.analysisToRun)},
576
- ${JSON.stringify({ additionalContext: rawParams.additionalRelevantContext })})`;
577
- }
578
- case RunAccessibilityTestTool_1.RunAccessibilityTestTool.NAME: {
579
- return `${rationale}await page.runAccessibilityTest();`;
580
- }
581
- case CreateBrowserCookieReportTool_1.CreateBrowserCookieReportTool.NAME: {
582
- return `${rationale}await page.ai.createCookieReport();`;
583
- }
584
- // All other tools delegate to the general 'run' method.
585
- default: {
586
- const toolName = proposedToolCall.name;
587
- const toolCallScript = hasNonEmptyParameters
588
- ? `${rationale}await page.run(${JSON.stringify(toolName)}, ${serializedParams});`
589
- : `${rationale}await page.run(${JSON.stringify(toolName)});`;
590
- return toolCallScript;
591
- }
592
- }
593
- }
594
- generateGitIgnore() {
595
- return `.DS_Store
596
- .idea
597
- .vscode
598
- node_modules
599
- # Test results generated by Playwright
600
- test-results
601
- playwright-report`;
602
- }
603
- /**
604
- * Generates the playwright.config.ts file with project dependencies
605
- */
606
- async generatePlaywrightConfig(graph, options) {
607
- const projects = [];
608
- // Create a project for each flow (both single and multiple flows in waves get individual projects)
609
- graph.executionOrder.forEach((wave) => {
610
- wave.forEach((flowId) => {
611
- const flow = graph.flows.get(flowId);
612
- const projectName = this.getProjectName(flow);
613
- const dependencies = this.getProjectDependencies(flowId, graph);
614
- projects.push(this.generateProjectConfig(projectName, flow, dependencies));
615
- });
616
- });
617
- const { areElementIdsVolatile, disableSelectorFailover, runInHeadedMode, slowMotionDelay, } = options;
618
- const useConfig = {
619
- screenshot: 'on',
620
- video: 'on',
621
- ...(runInHeadedMode && { headless: !runInHeadedMode }),
622
- ...(slowMotionDelay &&
623
- slowMotionDelay > 0 && { launchOptions: { slowMo: slowMotionDelay } }),
624
- };
625
- const selfHealingOptions = {
626
- areElementIdsVolatile,
627
- disableSelectorFailover,
628
- };
629
- const metadata = !options.disableSelfHealingTests
630
- ? `metadata: ${JSON.stringify({ selfHealingOptions: selfHealingOptions }, null, 2)}`
631
- : '';
632
- const config = `import { defineConfig, devices } from 'donobu';
633
-
634
- export default defineConfig({
635
- testDir: './tests',
636
- projects: [ ${projects.join(',')} ],
637
- use: ${JSON.stringify(useConfig, null, 2)},
638
- reporter: [
639
- ["github"],
640
- ["json", { outputFile: "test-results/playwright-report.json" }],
641
- ["html", { outputFolder: "playwright-report", open: "never" }],
642
- ],
643
- ${metadata}
644
- });`;
645
- return this.prettifyCode(config);
646
- }
647
- /**
648
- * Generates a single project configuration
649
- */
650
- generateProjectConfig(projectName, flow, dependencies) {
651
- const minimumTimeoutMilliseconds = 30000;
652
- const defaultTimeoutMilliseconds = 60000;
653
- const calculatedTimeout = flow.startedAt && flow.completedAt
654
- ? (flow.completedAt - flow.startedAt) * 2
655
- : defaultTimeoutMilliseconds;
656
- // Round up to the nearest 10000ms
657
- const timeoutMilliseconds = Math.max(minimumTimeoutMilliseconds, Math.ceil(calculatedTimeout / 10000) * 10000);
658
- const testMatch = `tests/${this.getTestFileName(flow)}`;
659
- const deps = dependencies.length > 0
660
- ? `\n dependencies: [${dependencies.map((d) => `'${d}'`).join(', ')}],`
661
- : '';
662
- // Get device name from flow config, default to 'Desktop Chromium'
663
- const deviceName = flow.browser?.using?.type === 'device'
664
- ? flow.browser.using.deviceName || 'Desktop Chromium'
665
- : 'Desktop Chromium';
666
- return `{
667
- name: '${projectName}',
668
- testMatch: '${testMatch}',${deps}
669
- use: { ...devices['${deviceName}'] },
670
- timeout: ${timeoutMilliseconds}
671
- }`;
672
- }
673
- /**
674
- * Gets project dependencies for a given flow
675
- */
676
- getProjectDependencies(flowId, graph) {
677
- const dependencies = graph.dependencies.get(flowId) || [];
678
- return dependencies.map((depId) => {
679
- const depFlow = graph.flows.get(depId);
680
- return this.getProjectName(depFlow);
681
- });
682
- }
683
- /**
684
- * Generates test files for all flows
685
- */
686
- async generateTestFiles(flowsWithToolCalls) {
687
- const files = [];
688
- for (const { metadata, toolCalls } of flowsWithToolCalls) {
689
- const fileName = this.getTestFileName(metadata);
690
- const content = await this.getFlowAsPlaywrightScript(metadata, toolCalls);
691
- files.push({
692
- path: `tests/${fileName}`,
693
- content,
694
- });
695
- }
696
- return files;
697
- }
698
- /**
699
- * Generates package.json
700
- */
701
- generatePackageJson() {
702
- return JSON.stringify({
703
- name: 'playwright-tests',
704
- version: '1.0.0',
705
- description: 'Playwright-based website tests made with Donobu',
706
- scripts: {
707
- test: 'npx playwright test',
708
- },
709
- devDependencies: {
710
- donobu: 'latest',
711
- playwright: PLAYWRIGHT_VERSION,
712
- '@playwright/test': PLAYWRIGHT_VERSION,
713
- },
714
- }, null, 2);
715
- }
716
- /**
717
- * Generates README.md
718
- */
719
- generateReadme() {
720
- return `# Playwright Tests
721
-
722
- This project contains [Playwright](https://playwright.dev/)-based tests made with [Donobu](https://www.donobu.com/).
723
-
724
- ## Installation
725
-
726
- Install project dependencies:
727
-
728
- \`\`\`bash
729
- npm install
730
- \`\`\`
731
-
732
- Install Playwright tooling (e.g. the web browsers for running tests)
733
-
734
- \`\`\`bash
735
- npx playwright install
736
- \`\`\`
737
-
738
- ## Running Tests
739
-
740
- \`\`\`bash
741
- npx playwright test
742
- \`\`\`
743
- `;
744
- }
745
- /**
746
- * Gets a project name for a flow
747
- */
748
- getProjectName(flow) {
749
- if (flow.name) {
750
- return flow.name.replace(/[^a-zA-Z0-9-_]/g, '-');
751
- }
752
- return `flow-${flow.id.substring(0, 8)}`;
753
- }
754
- /**
755
- * Gets a test file name for a flow
756
- */
757
- getTestFileName(flow) {
758
- const projectName = this.getProjectName(flow);
759
- return `${projectName}.spec.ts`;
760
- }
761
- async prettifyCode(code) {
762
- // Shenanigans way of importing prettier due to issues with Jest.
763
- // See https://github.com/jestjs/jest/issues/14305
764
- const prettier = require('prettier');
765
- const formattedCode = prettier.format(code, {
766
- parser: 'typescript',
767
- semi: true,
768
- singleQuote: true,
769
- });
770
- return formattedCode;
771
- }
772
- /**
773
- * Sanitizes a JSON string to be safely used within a template literal (backtick string)
774
- * Prevents both backtick termination and string interpolation from triggering
775
- *
776
- * @param jsonString - The JSON string to sanitize
777
- * @returns The sanitized string that can be safely used within backticks
778
- */
779
- sanitizeForTemplateLiteral(jsonString) {
780
- return (jsonString
781
- // Escape backticks to prevent template literal termination
782
- .replace(/`/g, '\\`')
783
- // Escape ${...} patterns to prevent string interpolation
784
- .replace(/\${/g, '\\${'));
785
- }
786
- isGptClientRequired(metadata, toolCalls, allTools) {
787
- return (metadata.resultJsonSchema !== null ||
788
- toolCalls
789
- .map((toolCall) => {
790
- return toolCall.name;
791
- })
792
- .map((toolCallName) => {
793
- return allTools.find((t) => t.name === toolCallName);
794
- })
795
- .filter((tool) => {
796
- return tool?.requiresGpt;
797
- }).length > 0);
798
- }
799
- /**
800
- * For tools that have a 'selector' argument, this function will serialize it
801
- * to an appropriate call to the DonobuExtendedPage#find function.
802
- */
803
- parseFindCall(args) {
804
- const selector = args.selector;
805
- const [primarySelector, ...failover] = selector.element;
806
- const frame = selector.frame;
807
- const findOptions = {};
808
- if (failover.length > 0) {
809
- findOptions.failover = failover;
810
- }
811
- if (frame) {
812
- findOptions.frame = frame;
813
- }
814
- // Build the page.find(...) call with 1 or 2 args depending on options presence.
815
- const hasOptions = Object.keys(findOptions).length > 0;
816
- const findCall = hasOptions
817
- ? `await page.find(${JSON.stringify(primarySelector)}, ${JSON.stringify(findOptions)})`
818
- : `await page.find(${JSON.stringify(primarySelector)})`;
819
- return findCall;
820
- }
821
- /**
822
- * Convert an *executed* list of {@link ToolCall}s into a list of
823
- * {@link ProposedToolCall}s that can be replayed in a fresh browser
824
- * session.
825
- *
826
- * ### What happens?
827
- * 1. **Identify calls that need selector-remapping.**
828
- * Some tools (e.g. {@link ClickTool}, {@link InputTextTool}) rely on a
829
- * deterministic selector captured at runtime. These tool names live in
830
- * `toolsThatNeedRemappedInputs`.
831
- * 2. **Attempt remap with {@link ReplayableInteraction.prepareToolCallForRerun}.**
832
- * If a call belongs to that set, we delegate to
833
- * `ReplayableInteraction.remapForRerun` to inject the stored selector and
834
- * strip any transient GPT-only fields.
835
- * 3. **Pass through other calls unchanged.**
836
- * Tools that have no UI footprint (e.g. navigation helpers, assertions)
837
- * are copied verbatim.
838
- * 4. **Error handling.**
839
- * When remapping fails (e.g. selector metadata missing) we *skip* the
840
- * offending call, emit a warning via `appLogger`, and continue processing
841
- * the remainder so that a “best-effort” replay is still possible.
842
- *
843
- * The resulting array preserves the original order **minus** any calls that
844
- * could not be remapped.
845
- *
846
- * @param toolCalls – The chronologically ordered list of executed
847
- * {@link ToolCall}s.
848
- * @returns A list of {@link ProposedToolCall}s ready for replay.
849
- */
850
- static async prepareToolCallsForRerun(toolCalls, options) {
851
- // Tools that need special handling for their inputs during replay.
852
- const toolsThatNeedRemappedInputs = new Set([
853
- ChooseSelectOptionTool_1.ChooseSelectOptionTool.NAME,
854
- ClickTool_1.ClickTool.NAME,
855
- HoverOverElementTool_1.HoverOverElementTool.NAME,
856
- InputRandomizedEmailAddressTool_1.InputRandomizedEmailAddressTool.NAME,
857
- InputTextTool_1.InputTextTool.NAME,
858
- PressKeyTool_1.PressKeyTool.NAME,
859
- RememberPageTextTool_1.RememberPageTextTool.NAME,
860
- ScrollPageTool_1.ScrollPageTool.NAME,
861
- ]);
862
- const proposedToolCalls = [];
863
- for (const toolCall of toolCalls) {
864
- const needsRemappedInputs = toolsThatNeedRemappedInputs.has(toolCall.toolName);
865
- if (needsRemappedInputs) {
866
- try {
867
- proposedToolCalls.push(ReplayableInteraction_1.ReplayableInteraction.prepareToolCallForRerun(toolCall, options));
868
- }
869
- catch (e) {
870
- // Skip.
871
- Logger_1.appLogger.warn(`Failed to prepare tool call for rerun: ${JSON.stringify(toolCall)}`, e);
872
- }
873
- }
874
- else {
875
- // This is just a normal tool then.
876
- proposedToolCalls.push({
877
- name: toolCall.toolName,
878
- parameters: toolCall.parameters,
879
- });
880
- }
881
- }
882
- return proposedToolCalls;
883
- }
884
- }
885
- exports.CodeGenerator = CodeGenerator;
886
- //# sourceMappingURL=CodeGenerator.js.map