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.
- package/README.md +14 -15
- package/dist/apis/FlowsApi.d.ts.map +1 -1
- package/dist/apis/FlowsApi.js +1 -0
- package/dist/apis/FlowsApi.js.map +1 -1
- package/dist/assets/generated/version +1 -1
- package/dist/assets/openapi-schema.html +6 -0
- package/dist/assets/openapi-schema.yaml +4 -0
- package/dist/bindings/PageInteractionTracker.d.ts.map +1 -1
- package/dist/bindings/PageInteractionTracker.js +5 -4
- package/dist/bindings/PageInteractionTracker.js.map +1 -1
- package/dist/codegen/CodeGenerator.d.ts +35 -0
- package/dist/codegen/CodeGenerator.d.ts.map +1 -0
- package/dist/codegen/CodeGenerator.js +882 -0
- package/dist/codegen/CodeGenerator.js.map +1 -0
- package/dist/esm/apis/FlowsApi.d.ts.map +1 -1
- package/dist/esm/apis/FlowsApi.js +1 -0
- package/dist/esm/apis/FlowsApi.js.map +1 -1
- package/dist/esm/assets/generated/version +1 -1
- package/dist/esm/assets/openapi-schema.html +6 -0
- package/dist/esm/assets/openapi-schema.yaml +4 -0
- package/dist/esm/bindings/PageInteractionTracker.d.ts.map +1 -1
- package/dist/esm/bindings/PageInteractionTracker.js +5 -4
- package/dist/esm/bindings/PageInteractionTracker.js.map +1 -1
- package/dist/esm/codegen/CodeGenerator.d.ts +35 -0
- package/dist/esm/codegen/CodeGenerator.d.ts.map +1 -0
- package/dist/esm/codegen/CodeGenerator.js +882 -0
- package/dist/esm/codegen/CodeGenerator.js.map +1 -0
- package/dist/esm/init.js +0 -15
- package/dist/esm/init.js.map +1 -1
- package/dist/esm/lib/PageAi.d.ts +39 -14
- package/dist/esm/lib/PageAi.d.ts.map +1 -1
- package/dist/esm/lib/PageAi.js +50 -32
- package/dist/esm/lib/PageAi.js.map +1 -1
- package/dist/esm/lib/SmartSelector.d.ts +8 -0
- package/dist/esm/lib/SmartSelector.d.ts.map +1 -1
- package/dist/esm/lib/SmartSelector.js +2 -2
- package/dist/esm/lib/SmartSelector.js.map +1 -1
- package/dist/esm/lib/createDonobuExtendedPage.js +2 -2
- package/dist/esm/lib/createDonobuExtendedPage.js.map +1 -1
- package/dist/esm/lib/fixtures/gptClients.js +2 -2
- package/dist/esm/lib/fixtures/gptClients.js.map +1 -1
- package/dist/esm/lib/pageAi/cache.d.ts +19 -23
- package/dist/esm/lib/pageAi/cache.d.ts.map +1 -1
- package/dist/esm/lib/pageAi/cache.js +105 -44
- package/dist/esm/lib/pageAi/cache.js.map +1 -1
- package/dist/esm/lib/pageAi/cacheEntryBuilder.d.ts +1 -3
- package/dist/esm/lib/pageAi/cacheEntryBuilder.d.ts.map +1 -1
- package/dist/esm/lib/pageAi/cacheEntryBuilder.js +6 -1
- package/dist/esm/lib/pageAi/cacheEntryBuilder.js.map +1 -1
- package/dist/esm/lib/testExtension.d.ts.map +1 -1
- package/dist/esm/lib/testExtension.js +2 -1
- package/dist/esm/lib/testExtension.js.map +1 -1
- package/dist/esm/lib/utils/selfHealing.js +1 -0
- package/dist/esm/lib/utils/selfHealing.js.map +1 -1
- package/dist/esm/lib/utils/triageTestFailure.d.ts.map +1 -1
- package/dist/esm/lib/utils/triageTestFailure.js +3 -2
- package/dist/esm/lib/utils/triageTestFailure.js.map +1 -1
- package/dist/esm/main.d.ts +4 -14
- package/dist/esm/main.d.ts.map +1 -1
- package/dist/esm/main.js +50 -35
- package/dist/esm/main.js.map +1 -1
- package/dist/esm/managers/AdminApiController.js +1 -1
- package/dist/esm/managers/AdminApiController.js.map +1 -1
- package/dist/esm/managers/DonobuFlow.js +4 -4
- package/dist/esm/managers/DonobuFlow.js.map +1 -1
- package/dist/esm/managers/DonobuFlowsManager.d.ts +33 -3
- package/dist/esm/managers/DonobuFlowsManager.d.ts.map +1 -1
- package/dist/esm/managers/DonobuFlowsManager.js +91 -7
- package/dist/esm/managers/DonobuFlowsManager.js.map +1 -1
- package/dist/esm/managers/GptConfigsManager.js +2 -2
- package/dist/esm/managers/GptConfigsManager.js.map +1 -1
- package/dist/esm/managers/RequestContextHolder.d.ts +3 -2
- package/dist/esm/managers/RequestContextHolder.d.ts.map +1 -1
- package/dist/esm/managers/RequestContextHolder.js +6 -9
- package/dist/esm/managers/RequestContextHolder.js.map +1 -1
- package/dist/esm/managers/ToolManager.js +1 -1
- package/dist/esm/managers/ToolManager.js.map +1 -1
- package/dist/esm/models/CodeGenerationOptions.d.ts +4 -0
- package/dist/esm/models/CodeGenerationOptions.d.ts.map +1 -1
- package/dist/esm/models/CodeGenerationOptions.js +4 -0
- package/dist/esm/models/CodeGenerationOptions.js.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistence.d.ts +5 -1
- package/dist/esm/persistence/flows/FlowsPersistence.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.d.ts +2 -1
- package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.js +50 -1
- package/dist/esm/persistence/flows/FlowsPersistenceAwsS3.js.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.d.ts +2 -1
- package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.js +31 -1
- package/dist/esm/persistence/flows/FlowsPersistenceFilesystem.js.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts +2 -1
- package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.js +23 -1
- package/dist/esm/persistence/flows/FlowsPersistenceGoogleCloudStorage.js.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts +2 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +7 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSupabase.d.ts +2 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSupabase.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSupabase.js +6 -1
- package/dist/esm/persistence/flows/FlowsPersistenceSupabase.js.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.d.ts +2 -1
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.d.ts.map +1 -1
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +17 -1
- package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js.map +1 -1
- package/dist/esm/utils/createTool.d.ts +2 -0
- package/dist/esm/utils/createTool.d.ts.map +1 -1
- package/dist/esm/utils/createTool.js +2 -0
- package/dist/esm/utils/createTool.js.map +1 -1
- package/dist/init.js +0 -15
- package/dist/init.js.map +1 -1
- package/dist/lib/PageAi.d.ts +39 -14
- package/dist/lib/PageAi.d.ts.map +1 -1
- package/dist/lib/PageAi.js +50 -32
- package/dist/lib/PageAi.js.map +1 -1
- package/dist/lib/SmartSelector.d.ts +8 -0
- package/dist/lib/SmartSelector.d.ts.map +1 -1
- package/dist/lib/SmartSelector.js +2 -2
- package/dist/lib/SmartSelector.js.map +1 -1
- package/dist/lib/createDonobuExtendedPage.js +2 -2
- package/dist/lib/createDonobuExtendedPage.js.map +1 -1
- package/dist/lib/fixtures/gptClients.js +2 -2
- package/dist/lib/fixtures/gptClients.js.map +1 -1
- package/dist/lib/pageAi/cache.d.ts +19 -23
- package/dist/lib/pageAi/cache.d.ts.map +1 -1
- package/dist/lib/pageAi/cache.js +105 -44
- package/dist/lib/pageAi/cache.js.map +1 -1
- package/dist/lib/pageAi/cacheEntryBuilder.d.ts +1 -3
- package/dist/lib/pageAi/cacheEntryBuilder.d.ts.map +1 -1
- package/dist/lib/pageAi/cacheEntryBuilder.js +6 -1
- package/dist/lib/pageAi/cacheEntryBuilder.js.map +1 -1
- package/dist/lib/testExtension.d.ts.map +1 -1
- package/dist/lib/testExtension.js +2 -1
- package/dist/lib/testExtension.js.map +1 -1
- package/dist/lib/utils/selfHealing.js +1 -0
- package/dist/lib/utils/selfHealing.js.map +1 -1
- package/dist/lib/utils/triageTestFailure.d.ts.map +1 -1
- package/dist/lib/utils/triageTestFailure.js +3 -2
- package/dist/lib/utils/triageTestFailure.js.map +1 -1
- package/dist/main.d.ts +4 -14
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +50 -35
- package/dist/main.js.map +1 -1
- package/dist/managers/AdminApiController.js +1 -1
- package/dist/managers/AdminApiController.js.map +1 -1
- package/dist/managers/DonobuFlow.js +4 -4
- package/dist/managers/DonobuFlow.js.map +1 -1
- package/dist/managers/DonobuFlowsManager.d.ts +33 -3
- package/dist/managers/DonobuFlowsManager.d.ts.map +1 -1
- package/dist/managers/DonobuFlowsManager.js +91 -7
- package/dist/managers/DonobuFlowsManager.js.map +1 -1
- package/dist/managers/GptConfigsManager.js +2 -2
- package/dist/managers/GptConfigsManager.js.map +1 -1
- package/dist/managers/RequestContextHolder.d.ts +3 -2
- package/dist/managers/RequestContextHolder.d.ts.map +1 -1
- package/dist/managers/RequestContextHolder.js +6 -9
- package/dist/managers/RequestContextHolder.js.map +1 -1
- package/dist/managers/ToolManager.js +1 -1
- package/dist/managers/ToolManager.js.map +1 -1
- package/dist/models/CodeGenerationOptions.d.ts +4 -0
- package/dist/models/CodeGenerationOptions.d.ts.map +1 -1
- package/dist/models/CodeGenerationOptions.js +4 -0
- package/dist/models/CodeGenerationOptions.js.map +1 -1
- package/dist/persistence/flows/FlowsPersistence.d.ts +5 -1
- package/dist/persistence/flows/FlowsPersistence.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceAwsS3.d.ts +2 -1
- package/dist/persistence/flows/FlowsPersistenceAwsS3.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceAwsS3.js +50 -1
- package/dist/persistence/flows/FlowsPersistenceAwsS3.js.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceFilesystem.d.ts +2 -1
- package/dist/persistence/flows/FlowsPersistenceFilesystem.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceFilesystem.js +31 -1
- package/dist/persistence/flows/FlowsPersistenceFilesystem.js.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts +2 -1
- package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.js +23 -1
- package/dist/persistence/flows/FlowsPersistenceGoogleCloudStorage.js.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts +2 -1
- package/dist/persistence/flows/FlowsPersistenceSqlite.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceSqlite.js +7 -1
- package/dist/persistence/flows/FlowsPersistenceSqlite.js.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceSupabase.d.ts +2 -1
- package/dist/persistence/flows/FlowsPersistenceSupabase.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceSupabase.js +6 -1
- package/dist/persistence/flows/FlowsPersistenceSupabase.js.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceVolatile.d.ts +2 -1
- package/dist/persistence/flows/FlowsPersistenceVolatile.d.ts.map +1 -1
- package/dist/persistence/flows/FlowsPersistenceVolatile.js +17 -1
- package/dist/persistence/flows/FlowsPersistenceVolatile.js.map +1 -1
- package/dist/utils/createTool.d.ts +2 -0
- package/dist/utils/createTool.d.ts.map +1 -1
- package/dist/utils/createTool.js +2 -0
- package/dist/utils/createTool.js.map +1 -1
- package/package.json +4 -6
- package/dist/assets/icon.png +0 -0
- package/dist/esm/assets/icon.png +0 -0
- package/dist/esm/managers/CodeGenerator.d.ts +0 -116
- package/dist/esm/managers/CodeGenerator.d.ts.map +0 -1
- package/dist/esm/managers/CodeGenerator.js +0 -886
- package/dist/esm/managers/CodeGenerator.js.map +0 -1
- package/dist/managers/CodeGenerator.d.ts +0 -116
- package/dist/managers/CodeGenerator.d.ts.map +0 -1
- package/dist/managers/CodeGenerator.js +0 -886
- 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
|