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