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