@empiricalrun/test-gen 0.7.3 → 0.8.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/actions/assertTextVisibility.d.ts +4 -0
  3. package/dist/actions/assertTextVisibility.d.ts.map +1 -0
  4. package/dist/actions/assertTextVisibility.js +44 -0
  5. package/dist/actions/click.d.ts +4 -0
  6. package/dist/actions/click.d.ts.map +1 -0
  7. package/dist/actions/click.js +40 -0
  8. package/dist/actions/done.d.ts +4 -0
  9. package/dist/actions/done.d.ts.map +1 -0
  10. package/dist/actions/done.js +31 -0
  11. package/dist/actions/fill.d.ts +4 -0
  12. package/dist/actions/fill.d.ts.map +1 -0
  13. package/dist/actions/fill.js +42 -0
  14. package/dist/actions/index.d.ts +12 -0
  15. package/dist/actions/index.d.ts.map +1 -0
  16. package/dist/actions/index.js +49 -0
  17. package/dist/agent/index.d.ts +7 -0
  18. package/dist/agent/index.d.ts.map +1 -0
  19. package/dist/agent/index.js +57 -0
  20. package/dist/bin/ai/index.d.ts +5 -1
  21. package/dist/bin/ai/index.d.ts.map +1 -1
  22. package/dist/bin/ai/index.js +3 -2
  23. package/dist/bin/ai/prompts/provider/index.d.ts +5 -0
  24. package/dist/bin/ai/prompts/provider/index.d.ts.map +1 -1
  25. package/dist/bin/ai/prompts/provider/index.js +12 -1
  26. package/dist/bin/ai/trace/index.d.ts +3 -3
  27. package/dist/bin/ai/trace/index.d.ts.map +1 -1
  28. package/dist/bin/ai/trace/index.js +1 -1
  29. package/dist/bin/index.js +45 -7
  30. package/dist/bin/reporter/ci.d.ts +1 -1
  31. package/dist/bin/reporter/ci.d.ts.map +1 -1
  32. package/dist/bin/scenarios/index.d.ts +3 -3
  33. package/dist/bin/scenarios/index.d.ts.map +1 -1
  34. package/dist/bin/scenarios/index.js +7 -7
  35. package/dist/bin/utils/context.d.ts +6 -0
  36. package/dist/bin/utils/context.d.ts.map +1 -0
  37. package/dist/bin/utils/context.js +24 -0
  38. package/dist/bin/utils/fs/index.d.ts +3 -3
  39. package/dist/bin/utils/fs/index.d.ts.map +1 -1
  40. package/dist/bin/utils/fs/index.js +10 -10
  41. package/dist/file/client.d.ts +10 -0
  42. package/dist/file/client.d.ts.map +1 -0
  43. package/dist/file/client.js +24 -0
  44. package/dist/file/server.d.ts +11 -0
  45. package/dist/file/server.d.ts.map +1 -0
  46. package/dist/file/server.js +50 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +23 -0
  50. package/dist/types/index.d.ts +25 -0
  51. package/dist/types/index.d.ts.map +1 -0
  52. package/dist/utils/exec.d.ts +4 -0
  53. package/dist/utils/exec.d.ts.map +1 -0
  54. package/dist/utils/exec.js +25 -0
  55. package/dist/utils/file.d.ts +2 -0
  56. package/dist/utils/file.d.ts.map +1 -0
  57. package/dist/utils/file.js +25 -0
  58. package/dist/utils/html.d.ts +4 -0
  59. package/dist/utils/html.d.ts.map +1 -0
  60. package/dist/utils/html.js +31 -0
  61. package/dist/utils/string.d.ts +2 -0
  62. package/dist/utils/string.d.ts.map +1 -0
  63. package/dist/utils/string.js +9 -0
  64. package/package.json +9 -1
  65. package/dist/bin/types/index.d.ts +0 -15
  66. package/dist/bin/types/index.d.ts.map +0 -1
  67. /package/dist/{bin/types → types}/index.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - a7e8c1c: fix: locator issues while generating tests
8
+
9
+ ## 0.8.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 30f61bc: feat: add support for browsing agent
14
+
15
+ ### Patch Changes
16
+
17
+ - 71ffe00: feat: respect gitignore while building prompt context
18
+
3
19
  ## 0.7.3
4
20
 
5
21
  ### Patch Changes
@@ -0,0 +1,4 @@
1
+ import { PlaywrightActionGenerator } from "../types";
2
+ export declare const PLAYWRIGHT_ASSERT_TEXT_VISIBILITY_ACTION_NAME = "assert_text_visibility";
3
+ export declare const assertTextVisibilityActionGenerator: PlaywrightActionGenerator;
4
+ //# sourceMappingURL=assertTextVisibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertTextVisibility.d.ts","sourceRoot":"","sources":["../../src/actions/assertTextVisibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,6CAA6C,2BAChC,CAAC;AAE3B,eAAO,MAAM,mCAAmC,EAAE,yBA0CjD,CAAC"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertTextVisibilityActionGenerator = exports.PLAYWRIGHT_ASSERT_TEXT_VISIBILITY_ACTION_NAME = void 0;
4
+ exports.PLAYWRIGHT_ASSERT_TEXT_VISIBILITY_ACTION_NAME = "assert_text_visibility";
5
+ const assertTextVisibilityActionGenerator = (page) => {
6
+ return {
7
+ execute: async (args) => {
8
+ await page
9
+ .locator(args.css_selector)
10
+ .first()
11
+ .getByText(args.text)
12
+ .isVisible({ timeout: 3000 });
13
+ },
14
+ // TODO: args transformer to be kept at a single place
15
+ template: (args) => `await expect(page.locator("${args.css_selector}").first().getByText("${args.text}")).toBeVisible();`,
16
+ name: exports.PLAYWRIGHT_ASSERT_TEXT_VISIBILITY_ACTION_NAME,
17
+ schema: {
18
+ type: "function",
19
+ function: {
20
+ name: exports.PLAYWRIGHT_ASSERT_TEXT_VISIBILITY_ACTION_NAME,
21
+ description: "assert whether the given element on the page is visible",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ text: {
26
+ type: "string",
27
+ description: "text to be asserted",
28
+ },
29
+ css_selector: {
30
+ type: "string",
31
+ description: "CSS selector to identify the element uniquely and click it. When creating CSS selectors, ensure they are unique and specific enough to select only one element, even if there are multiple elements of the same type (like multiple h1 elements)",
32
+ },
33
+ reason: {
34
+ type: "string",
35
+ description: "reason for calling this function",
36
+ },
37
+ },
38
+ required: ["css_selector", "text", "reason"],
39
+ },
40
+ },
41
+ },
42
+ };
43
+ };
44
+ exports.assertTextVisibilityActionGenerator = assertTextVisibilityActionGenerator;
@@ -0,0 +1,4 @@
1
+ import { PlaywrightActionGenerator } from "../types";
2
+ export declare const PLAYWRIGHT_CLICK_ACTION_NAME = "click_element";
3
+ export declare const clickActionGenerator: PlaywrightActionGenerator;
4
+ //# sourceMappingURL=click.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../src/actions/click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,4BAA4B,kBAAkB,CAAC;AAE5D,eAAO,MAAM,oBAAoB,EAAE,yBAoClC,CAAC"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clickActionGenerator = exports.PLAYWRIGHT_CLICK_ACTION_NAME = void 0;
4
+ exports.PLAYWRIGHT_CLICK_ACTION_NAME = "click_element";
5
+ const clickActionGenerator = (page) => {
6
+ return {
7
+ execute: async (args) => {
8
+ await page
9
+ .locator(args.css_selector.split(" ")[0].replaceAll("\\", ""))
10
+ .first()
11
+ .click({ timeout: 3000 });
12
+ await page.waitForTimeout(3000);
13
+ },
14
+ // TODO: args transformer to be kept at a single place
15
+ template: (args) => `await page.locator("${args.css_selector.split(" ")[0].replaceAll("\\", "")}").first().click();`,
16
+ name: exports.PLAYWRIGHT_CLICK_ACTION_NAME,
17
+ schema: {
18
+ type: "function",
19
+ function: {
20
+ name: exports.PLAYWRIGHT_CLICK_ACTION_NAME,
21
+ description: "click an element on the page",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ css_selector: {
26
+ type: "string",
27
+ description: "CSS selector to identify the element uniquely and click it. When creating CSS selectors, ensure they are unique and specific enough to select only one element, even if there are multiple elements of the same type (like multiple h1 elements)",
28
+ },
29
+ reason: {
30
+ type: "string",
31
+ description: "reason for calling this function",
32
+ },
33
+ },
34
+ required: ["css_selector", "reason"],
35
+ },
36
+ },
37
+ },
38
+ };
39
+ };
40
+ exports.clickActionGenerator = clickActionGenerator;
@@ -0,0 +1,4 @@
1
+ import { PlaywrightActionGenerator } from "../types";
2
+ export declare const PLAYWRIGHT_DONE_ACTION_NAME = "task_done";
3
+ export declare const doneActionGenerator: PlaywrightActionGenerator;
4
+ //# sourceMappingURL=done.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"done.d.ts","sourceRoot":"","sources":["../../src/actions/done.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,2BAA2B,cAAc,CAAC;AAEvD,eAAO,MAAM,mBAAmB,EAAE,yBAyBjC,CAAC"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.doneActionGenerator = exports.PLAYWRIGHT_DONE_ACTION_NAME = void 0;
4
+ exports.PLAYWRIGHT_DONE_ACTION_NAME = "task_done";
5
+ const doneActionGenerator = (page) => {
6
+ return {
7
+ execute: async () => {
8
+ await page.close();
9
+ },
10
+ template: () => ``,
11
+ name: exports.PLAYWRIGHT_DONE_ACTION_NAME,
12
+ schema: {
13
+ type: "function",
14
+ function: {
15
+ name: exports.PLAYWRIGHT_DONE_ACTION_NAME,
16
+ description: "end the task by calling this method",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ reason: {
21
+ type: "string",
22
+ description: "reason for declaring the task is complete",
23
+ },
24
+ },
25
+ required: [],
26
+ },
27
+ },
28
+ },
29
+ };
30
+ };
31
+ exports.doneActionGenerator = doneActionGenerator;
@@ -0,0 +1,4 @@
1
+ import { PlaywrightActionGenerator } from "../types";
2
+ export declare const PLAYWRIGHT_FILL_ACTION_NAME = "fill_input_element";
3
+ export declare const fillActionGenerator: PlaywrightActionGenerator;
4
+ //# sourceMappingURL=fill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fill.d.ts","sourceRoot":"","sources":["../../src/actions/fill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,2BAA2B,uBAAuB,CAAC;AAEhE,eAAO,MAAM,mBAAmB,EAAE,yBAsCjC,CAAC"}
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fillActionGenerator = exports.PLAYWRIGHT_FILL_ACTION_NAME = void 0;
4
+ exports.PLAYWRIGHT_FILL_ACTION_NAME = "fill_input_element";
5
+ const fillActionGenerator = (page) => {
6
+ return {
7
+ execute: async (args) => {
8
+ await page
9
+ .locator(args.css_selector.split(" ")[0])
10
+ .fill(args.text, { timeout: 3000 });
11
+ },
12
+ // TODO: args transformer to be kept at a single place
13
+ template: (args) => `await page.locator("${args.css_selector.split(" ")[0]}").fill("${args.text}");`,
14
+ name: exports.PLAYWRIGHT_FILL_ACTION_NAME,
15
+ schema: {
16
+ type: "function",
17
+ function: {
18
+ name: exports.PLAYWRIGHT_FILL_ACTION_NAME,
19
+ description: "fill the input element with a particular text",
20
+ parameters: {
21
+ type: "object",
22
+ properties: {
23
+ text: {
24
+ type: "string",
25
+ description: "The text to fill the input element with",
26
+ },
27
+ css_selector: {
28
+ type: "string",
29
+ description: "CSS selector to identify the element uniquely.When creating CSS selectors, ensure they are unique to the page and specific enough to select only one element",
30
+ },
31
+ reason: {
32
+ type: "string",
33
+ description: "reason for calling this function",
34
+ },
35
+ },
36
+ required: ["text", "css_selector", "reason"],
37
+ },
38
+ },
39
+ },
40
+ };
41
+ };
42
+ exports.fillActionGenerator = fillActionGenerator;
@@ -0,0 +1,12 @@
1
+ import { Page } from "playwright";
2
+ import { ActionSchema } from "../types";
3
+ export declare class PlaywrightActions {
4
+ private actions;
5
+ private recordedActions;
6
+ constructor(page: Page);
7
+ executeAction(name: string | undefined, args: Record<string, any>): Promise<void>;
8
+ getActionSchemas(): ActionSchema[];
9
+ generateCode(): string;
10
+ isComplete(): boolean;
11
+ }
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/actions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,OAAO,EAAU,YAAY,EAAE,MAAM,UAAU,CAAC;AAMhD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,eAAe,CAAmC;gBAC9C,IAAI,EAAE,IAAI;IAUhB,aAAa,CAAC,IAAI,oBAAa,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAkBhE,gBAAgB,IAAI,YAAY,EAAE;IAIlC,YAAY;IAIZ,UAAU;CAMX"}
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PlaywrightActions = void 0;
4
+ const assertTextVisibility_1 = require("./assertTextVisibility");
5
+ const click_1 = require("./click");
6
+ const done_1 = require("./done");
7
+ const fill_1 = require("./fill");
8
+ class PlaywrightActions {
9
+ actions;
10
+ recordedActions;
11
+ constructor(page) {
12
+ this.actions = [
13
+ (0, fill_1.fillActionGenerator)(page),
14
+ (0, click_1.clickActionGenerator)(page),
15
+ (0, done_1.doneActionGenerator)(page),
16
+ (0, assertTextVisibility_1.assertTextVisibilityActionGenerator)(page),
17
+ ];
18
+ this.recordedActions = [];
19
+ }
20
+ async executeAction(name = "", args) {
21
+ const [action] = this.actions.filter((a) => a.name === name);
22
+ if (!action) {
23
+ throw Error(`No action registered for action: ${name}`);
24
+ }
25
+ try {
26
+ console.log("executing:", name, "\nreason:", args.reason);
27
+ await action.execute(args);
28
+ // record successful actions
29
+ const code = action.template(args);
30
+ this.recordedActions.push({ name, code });
31
+ console.log(`code: ${code}`, "\n\n");
32
+ }
33
+ catch (e) {
34
+ // TODO: make these specific errors so that its easy to consume
35
+ throw Error(`Error executing ${name} action of playwright: ${e}`);
36
+ }
37
+ }
38
+ getActionSchemas() {
39
+ return this.actions.map((a) => a.schema);
40
+ }
41
+ generateCode() {
42
+ return this.recordedActions.map((a) => a.code).join("\n");
43
+ }
44
+ isComplete() {
45
+ const [doneAction] = this.recordedActions.filter((a) => a.name === done_1.PLAYWRIGHT_DONE_ACTION_NAME);
46
+ return !!doneAction;
47
+ }
48
+ }
49
+ exports.PlaywrightActions = PlaywrightActions;
@@ -0,0 +1,7 @@
1
+ import { Page } from "playwright";
2
+ export declare function browsingAgent(task: string, page: Page, options?: {
3
+ htmlSanitize?: {
4
+ disallowedStrings?: string[];
5
+ };
6
+ }): Promise<string>;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAQlC,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,EACV,OAAO,GAAE;IACP,YAAY,CAAC,EAAE;QACb,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACE,mBAsDP"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.browsingAgent = void 0;
7
+ const actions_1 = require("../actions");
8
+ const ai_1 = require("../bin/ai");
9
+ const provider_1 = require("../bin/ai/prompts/provider");
10
+ const trace_1 = __importDefault(require("../bin/ai/trace"));
11
+ const html_1 = require("../utils/html");
12
+ async function browsingAgent(task, page, options = {}) {
13
+ const trace = new trace_1.default({ name: "browsing-agent" });
14
+ const actions = new actions_1.PlaywrightActions(page);
15
+ const tools = actions.getActionSchemas();
16
+ let isTaskDone = false;
17
+ const previousActions = [];
18
+ while (!isTaskDone) {
19
+ trace.update({ input: { task } });
20
+ const pageContentSpan = trace.startSpan("page-content");
21
+ const pageContent = await page.content();
22
+ pageContentSpan.end({ output: { pageContent } });
23
+ const sanitizationSpan = trace.startSpan("page-sanitization");
24
+ const pageSnapshot = (0, html_1.sanitizeHtml)(pageContent, options.htmlSanitize);
25
+ sanitizationSpan.end({ output: { pageSnapshot } });
26
+ const promptSpan = trace.startSpan("page-prompt");
27
+ const messages = await (0, provider_1.getPromptForNextAction)({
28
+ pageSnapshot,
29
+ previousActions,
30
+ task,
31
+ });
32
+ promptSpan.end({ output: { messages } });
33
+ const completion = await (0, ai_1.getLLMResult)({
34
+ messages,
35
+ tools,
36
+ trace,
37
+ });
38
+ const toolCalls = completion?.tool_calls || [];
39
+ for (const i in toolCalls) {
40
+ const toolCall = toolCalls[i];
41
+ try {
42
+ await actions.executeAction(toolCall.function.name, JSON.parse(toolCall.function.arguments));
43
+ previousActions.push(JSON.stringify(toolCall));
44
+ }
45
+ catch (e) {
46
+ // TODO: implement feedback loop to llm
47
+ console.error(e);
48
+ }
49
+ }
50
+ isTaskDone = actions.isComplete();
51
+ }
52
+ const code = actions.generateCode();
53
+ trace.update({ input: { task }, output: { code } });
54
+ console.log("Successfully generated code for the given task", "[Trace:", trace.url, "]");
55
+ return code;
56
+ }
57
+ exports.browsingAgent = browsingAgent;
@@ -1,4 +1,8 @@
1
1
  import OpenAI from "openai";
2
2
  import LLMTracing from "./trace";
3
- export declare function getLLMResult(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], trace?: LLMTracing): Promise<string>;
3
+ export declare function getLLMResult({ messages, trace, tools, }: {
4
+ messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
5
+ trace?: LLMTracing;
6
+ tools?: OpenAI.Chat.Completions.ChatCompletionTool[];
7
+ }): Promise<OpenAI.Chat.Completions.ChatCompletionMessage | undefined>;
4
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/ai/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,UAAU,MAAM,SAAS,CAAC;AAEjC,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,0BAA0B,EAAE,EAC9D,KAAK,CAAC,EAAE,UAAU,GACjB,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/ai/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,UAAU,MAAM,SAAS,CAAC;AAEjC,wBAAsB,YAAY,CAAC,EACjC,QAAQ,EACR,KAAK,EACL,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,0BAA0B,EAAE,CAAC;IAC/D,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,kBAAkB,EAAE,CAAC;CACtD,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAqBrE"}
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getLLMResult = void 0;
7
7
  const openai_1 = __importDefault(require("openai"));
8
- async function getLLMResult(messages, trace) {
8
+ async function getLLMResult({ messages, trace, tools, }) {
9
9
  const openai = new openai_1.default();
10
10
  const model = "gpt-4o";
11
11
  const parameters = {
@@ -20,9 +20,10 @@ async function getLLMResult(messages, trace) {
20
20
  const completion = await openai.chat.completions.create({
21
21
  messages,
22
22
  model,
23
+ tools,
23
24
  ...parameters,
24
25
  });
25
- const output = completion.choices[0]?.message.content || "";
26
+ const output = completion.choices[0]?.message;
26
27
  generation?.end({ output });
27
28
  return output;
28
29
  }
@@ -1,3 +1,8 @@
1
1
  import OpenAI from "openai";
2
2
  export declare function getPrompt(name: string, vars: any): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam[]>;
3
+ export declare function getPromptForNextAction({ pageSnapshot, task, previousActions, }: {
4
+ pageSnapshot: string;
5
+ task: string;
6
+ previousActions: string[];
7
+ }): Promise<OpenAI.Chat.Completions.ChatCompletionMessageParam[]>;
3
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/bin/ai/prompts/provider/index.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAS5B,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,GACR,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,0BAA0B,EAAE,CAAC,CAI/D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/bin/ai/prompts/provider/index.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAS5B,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,GAAG,GACR,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,0BAA0B,EAAE,CAAC,CAI/D;AAGD,wBAAsB,sBAAsB,CAAC,EAC3C,YAAiB,EACjB,IAAS,EACT,eAAoB,GACrB,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,iEAQA"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getPrompt = void 0;
3
+ exports.getPromptForNextAction = exports.getPrompt = void 0;
4
4
  const langfuse_1 = require("langfuse");
5
5
  const langfuse = new langfuse_1.Langfuse({
6
6
  secretKey: "sk-lf-30eee545-9c2c-4646-bfd9-8f38acf2fa8f",
@@ -14,3 +14,14 @@ async function getPrompt(name, vars) {
14
14
  return compiledPrompt;
15
15
  }
16
16
  exports.getPrompt = getPrompt;
17
+ // TODO: move this prompt to langfuse
18
+ async function getPromptForNextAction({ pageSnapshot = "", task = "", previousActions = [], }) {
19
+ const previousActionsStr = previousActions.join("\n\n ---- \n\n");
20
+ const prompt = await getPrompt("browsing-agent-next-action", {
21
+ pageSnapshot,
22
+ previousActionsStr,
23
+ task,
24
+ });
25
+ return prompt;
26
+ }
27
+ exports.getPromptForNextAction = getPromptForNextAction;
@@ -1,8 +1,8 @@
1
1
  import OpenAI from "openai";
2
2
  export default class LLMTracing {
3
3
  private trace;
4
- constructor({ name }?: {
5
- name?: string;
4
+ constructor({ name }: {
5
+ name: string;
6
6
  });
7
7
  get id(): string;
8
8
  get url(): string;
@@ -29,7 +29,7 @@ export default class LLMTracing {
29
29
  input: any;
30
30
  }): {
31
31
  end: ({ output, usage, }: {
32
- output: string;
32
+ output: any;
33
33
  usage?: OpenAI.Completions.CompletionUsage | undefined;
34
34
  }) => void;
35
35
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bin/ai/trace/index.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAe5B,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,OAAO,CAAC,KAAK,CAAsB;gBACvB,EAAE,IAAuB,EAAE,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO;IAS/D,IAAI,EAAE,WAEL;IAED,IAAI,GAAG,WAEN;IAED,KAAK,CAAC,EACJ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,MAAM,GACP,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,GAAG,CAAC;QACZ,QAAQ,CAAC,EAAE,GAAG,CAAC;QACf,MAAM,CAAC,EAAE,GAAG,CAAC;KACd;IAID,MAAM,CAAC,EACL,KAAU,EACV,MAAW,EACX,QAAa,GACd,EAAE;QACD,KAAK,CAAC,EAAE,GAAG,CAAC;QACZ,MAAM,CAAC,EAAE,GAAG,CAAC;QACb,QAAQ,CAAC,EAAE,GAAG,CAAC;KAChB;IAID,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAC/B,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;;oBAIJ,OAAO,MAAM,EAAE,GAAG,CAAC;;;IAMnD,eAAe,CAAC,EACd,IAAS,EACT,KAAU,EACV,UAAe,EACf,KAAU,GACX,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChC,KAAK,EAAE,GAAG,CAAC;KACZ;;oBAYa,MAAM;;cAEZ,IAAI;;CAcb"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bin/ai/trace/index.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAe5B,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,OAAO,CAAC,KAAK,CAAsB;gBACvB,EAAE,IAAI,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;IAStC,IAAI,EAAE,WAEL;IAED,IAAI,GAAG,WAEN;IAED,KAAK,CAAC,EACJ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,MAAM,GACP,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,GAAG,CAAC;QACZ,QAAQ,CAAC,EAAE,GAAG,CAAC;QACf,MAAM,CAAC,EAAE,GAAG,CAAC;KACd;IAID,MAAM,CAAC,EACL,KAAU,EACV,MAAW,EACX,QAAa,GACd,EAAE;QACD,KAAK,CAAC,EAAE,GAAG,CAAC;QACZ,MAAM,CAAC,EAAE,GAAG,CAAC;QACb,QAAQ,CAAC,EAAE,GAAG,CAAC;KAChB;IAID,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EAC/B,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;;oBAIJ,OAAO,MAAM,EAAE,GAAG,CAAC;;;IAMnD,eAAe,CAAC,EACd,IAAS,EACT,KAAU,EACV,UAAe,EACf,KAAU,GACX,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChC,KAAK,EAAE,GAAG,CAAC;KACZ;;oBAYa,GAAG;;cAET,IAAI;;CAcb"}
@@ -13,7 +13,7 @@ process.on("SIGINT", () => langfuse.flush());
13
13
  process.on("SIGTERM", async () => langfuse.flush());
14
14
  class LLMTracing {
15
15
  trace;
16
- constructor({ name = "test-generator" } = {}) {
16
+ constructor({ name }) {
17
17
  const sessionDetails = (0, session_1.getSessionDetails)();
18
18
  this.trace = langfuse.trace({
19
19
  name,
package/dist/bin/index.js CHANGED
@@ -4,24 +4,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const detect_port_1 = __importDefault(require("detect-port"));
7
8
  const dotenv_1 = __importDefault(require("dotenv"));
8
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const server_1 = require("../file/server");
11
+ const exec_1 = require("../utils/exec");
9
12
  const ai_1 = require("./ai");
10
13
  const provider_1 = require("./ai/prompts/provider");
11
14
  const trace_1 = __importDefault(require("./ai/trace"));
12
15
  const logger_1 = require("./logger");
13
16
  const ci_1 = require("./reporter/ci");
14
17
  const scenarios_1 = require("./scenarios");
15
- const fs_1 = require("./utils/fs");
18
+ const context_1 = require("./utils/context");
16
19
  const web_1 = require("./utils/platform/web");
17
20
  dotenv_1.default.config({
18
21
  path: [".env.local", ".env"],
19
22
  });
20
23
  const logger = new logger_1.CustomLogger();
21
24
  async function generateTest(scenarios, file, isUpdate) {
22
- const codePrompt = await (0, fs_1.generatePromptFromDirectory)("./tests");
23
- const pomPrompt = await (0, fs_1.generatePromptFromDirectory)("./pages");
24
- const testFileContent = fs_extra_1.default.readFileSync(file, "utf-8");
25
+ const context = await (0, context_1.contextForGeneration)(file);
26
+ const { codePrompt, pomPrompt, testFileContent } = context;
25
27
  const generatedScenarios = [];
26
28
  for (const i in scenarios) {
27
29
  logger.logEmptyLine();
@@ -53,7 +55,11 @@ async function generateTest(scenarios, file, isUpdate) {
53
55
  scenarioFile: file,
54
56
  });
55
57
  promptSpan.end({ output: { instruction } });
56
- let response = await (0, ai_1.getLLMResult)(instruction, trace);
58
+ const firstShotMessage = await (0, ai_1.getLLMResult)({
59
+ messages: instruction,
60
+ trace,
61
+ });
62
+ let response = firstShotMessage?.content || "";
57
63
  logger.success("Test generated successfully!");
58
64
  const readWriteFileSpan = trace.startSpan("write-to-file");
59
65
  let contents = fs_extra_1.default.readFileSync(file, "utf-8");
@@ -104,7 +110,8 @@ async function generateTest(scenarios, file, isUpdate) {
104
110
  scenaioName: scenario.name,
105
111
  });
106
112
  promptSpan.end({ output: { instruction } });
107
- response = await (0, ai_1.getLLMResult)(instruction, trace);
113
+ const message = await (0, ai_1.getLLMResult)({ messages: instruction, trace });
114
+ response = message?.content || "";
108
115
  const readWriteFileSpan = trace.startSpan("write-to-file");
109
116
  await fs_extra_1.default.writeFile(file, response, "utf-8");
110
117
  readWriteFileSpan.end({ output: { response } });
@@ -125,15 +132,46 @@ async function generateTest(scenarios, file, isUpdate) {
125
132
  }
126
133
  return generatedScenarios;
127
134
  }
135
+ async function generateTestsUsingBrowsingAgent() {
136
+ const testFilePath = process.argv[2];
137
+ if (!testFilePath) {
138
+ console.error("test file path is required");
139
+ process.exit(1);
140
+ }
141
+ const port = await (0, detect_port_1.default)(3030);
142
+ const fileService = new server_1.FileService({ port });
143
+ await fileService.startFileService();
144
+ try {
145
+ fileService.setFilePath(testFilePath);
146
+ //TODO: change this to per test
147
+ await (0, exec_1.cmd)([`npx`, `playwright`, `test`, testFilePath, "--headed"], {
148
+ env: {
149
+ APP_PORT: port.toString(),
150
+ PW_TEST_HTML_REPORT_OPEN: "never",
151
+ },
152
+ });
153
+ process.exit(0);
154
+ }
155
+ catch (e) {
156
+ console.error(e);
157
+ process.exit(1);
158
+ }
159
+ }
128
160
  (async function main() {
129
161
  if (process.argv.length < 3) {
130
162
  logger.error("Please provide path to scenarios using command:", "npx @empiricalrun/test-gen <SCENARIOS_FILE_PATH> <SCENARIO_NAME> -u");
131
163
  process.exit(1);
132
164
  }
133
165
  const scenariosPath = process.argv[2];
166
+ //TODO: come up with better way to handle this
167
+ if (scenariosPath.endsWith(".ts")) {
168
+ logger.success("Generating tests using browsing agent");
169
+ await generateTestsUsingBrowsingAgent();
170
+ return;
171
+ }
134
172
  const scenario = process.argv[3] === "-u" ? "" : process.argv[3];
135
173
  const isUpdate = process.argv.includes("-u");
136
- let testGenConfigs = await (0, scenarios_1.generateScenarios)(scenariosPath);
174
+ let testGenConfigs = await (0, scenarios_1.loadScenarios)(scenariosPath);
137
175
  if (scenario) {
138
176
  // filter config
139
177
  testGenConfigs = testGenConfigs.filter((t) => t.scenarios.some((s) => s.name === scenario));
@@ -1,3 +1,3 @@
1
- import { Scenario } from "../types";
1
+ import { Scenario } from "../../types";
2
2
  export declare function reportOnCI(scenarios: Scenario[]): Promise<Scenario[]>;
3
3
  //# sourceMappingURL=ci.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../../../src/bin/reporter/ci.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAsB,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,uBAerD"}
1
+ {"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../../../src/bin/reporter/ci.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wBAAsB,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,uBAerD"}
@@ -1,7 +1,7 @@
1
- import { Scenario } from "../types";
2
- declare function generateScenarios(scenariosPath: string): Promise<{
1
+ import { Scenario } from "../../types";
2
+ declare function loadScenarios(scenariosPath: string): Promise<{
3
3
  specPath: string;
4
4
  scenarios: Scenario[];
5
5
  }[]>;
6
- export { generateScenarios };
6
+ export { loadScenarios };
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/scenarios/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAiGpC,iBAAe,iBAAiB,CAC9B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,QAAQ,EAAE,CAAA;CAAE,EAAE,CAAC,CA8BxD;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/scenarios/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAiGvC,iBAAe,aAAa,CAC1B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,QAAQ,EAAE,CAAA;CAAE,EAAE,CAAC,CA8BxD;AAED,OAAO,EAAE,aAAa,EAAE,CAAC"}
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateScenarios = void 0;
6
+ exports.loadScenarios = void 0;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const google_auth_library_1 = require("google-auth-library");
9
9
  const path_1 = __importDefault(require("path"));
@@ -23,7 +23,7 @@ function isValidJSON(str) {
23
23
  * @param path
24
24
  * @returns updated paths of scenarios
25
25
  */
26
- async function generateScenariosUsingGsheet(path) {
26
+ async function loadScenariosFromGsheet(path) {
27
27
  const { GoogleSpreadsheet } = await import("google-spreadsheet");
28
28
  const url = new URL(path);
29
29
  const docId = url.pathname.split("/")[3];
@@ -76,7 +76,7 @@ async function generateScenariosUsingGsheet(path) {
76
76
  }
77
77
  return results;
78
78
  }
79
- async function generateScenariosUsingYAML(scenariosPath) {
79
+ async function loadScenariosFromYAML(scenariosPath) {
80
80
  const file = await fs_extra_1.default.readFile(path_1.default.resolve(process.cwd(), scenariosPath), "utf8");
81
81
  const fileName = scenariosPath.split("/").pop()?.split(".")[0];
82
82
  const config = (0, yaml_1.parse)(file);
@@ -84,12 +84,12 @@ async function generateScenariosUsingYAML(scenariosPath) {
84
84
  config.specPath = `${fileDir}/${fileName}.spec.ts`;
85
85
  return [config];
86
86
  }
87
- async function generateScenarios(scenariosPath) {
87
+ async function loadScenarios(scenariosPath) {
88
88
  if (scenariosPath.startsWith("https://docs.google.com/spreadsheets")) {
89
- return await generateScenariosUsingGsheet(scenariosPath);
89
+ return await loadScenariosFromGsheet(scenariosPath);
90
90
  }
91
91
  else if (scenariosPath.endsWith(".yaml")) {
92
- return await generateScenariosUsingYAML(scenariosPath);
92
+ return await loadScenariosFromYAML(scenariosPath);
93
93
  }
94
94
  else if (isValidJSON(atob(scenariosPath))) {
95
95
  const str = atob(scenariosPath);
@@ -117,4 +117,4 @@ async function generateScenarios(scenariosPath) {
117
117
  }
118
118
  throw Error("Invalid path for test scenarios");
119
119
  }
120
- exports.generateScenarios = generateScenarios;
120
+ exports.loadScenarios = loadScenarios;
@@ -0,0 +1,6 @@
1
+ export declare function contextForGeneration(file: string): Promise<{
2
+ codePrompt: string | undefined;
3
+ pomPrompt: string | undefined;
4
+ testFileContent: string;
5
+ }>;
6
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/bin/utils/context.ts"],"names":[],"mappings":"AAKA,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM;;;;GAatD"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.contextForGeneration = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const ignore_1 = __importDefault(require("ignore"));
9
+ const fs_1 = require("./fs");
10
+ async function contextForGeneration(file) {
11
+ const ignoreFn = (0, ignore_1.default)();
12
+ if (fs_extra_1.default.existsSync(".gitignore")) {
13
+ // Not checking for nested gitignore
14
+ const gitignore = fs_extra_1.default.readFileSync(".gitignore").toString();
15
+ ignoreFn.add(gitignore);
16
+ }
17
+ const filter = ignoreFn.createFilter();
18
+ return {
19
+ codePrompt: await (0, fs_1.generatePromptFromDirectory)("./tests", filter),
20
+ pomPrompt: await (0, fs_1.generatePromptFromDirectory)("./pages", filter),
21
+ testFileContent: fs_extra_1.default.readFileSync(file, "utf-8"),
22
+ };
23
+ }
24
+ exports.contextForGeneration = contextForGeneration;
@@ -1,5 +1,5 @@
1
- import { FileContent } from "../../types";
2
- export declare function readFilesInDirectory(dir?: string): Promise<FileContent[]>;
1
+ import { FileContent } from "../../../types";
2
+ export declare function readFilesInDirectory(dir: string | undefined, filterFunc: (file: string) => boolean): Promise<FileContent[]>;
3
3
  export declare function convertFileContentsToString(files?: FileContent[]): string;
4
- export declare function generatePromptFromDirectory(dir?: string): Promise<string | undefined>;
4
+ export declare function generatePromptFromDirectory(dir: string | undefined, filterFunc: (file: string) => boolean): Promise<string | undefined>;
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bin/utils/fs/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,wBAAsB,oBAAoB,CAAC,GAAG,GAAE,MAAW,0BAgB1D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,GAAE,WAAW,EAAO,UAQpE;AAED,wBAAsB,2BAA2B,CAAC,GAAG,SAAK,+BAQzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bin/utils/fs/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,wBAAsB,oBAAoB,CACxC,GAAG,oBAAa,EAChB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,0BAgBtC;AAED,wBAAgB,2BAA2B,CAAC,KAAK,GAAE,WAAW,EAAO,UAQpE;AAED,wBAAsB,2BAA2B,CAC/C,GAAG,oBAAK,EACR,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,+BAStC"}
@@ -6,18 +6,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generatePromptFromDirectory = exports.convertFileContentsToString = exports.readFilesInDirectory = void 0;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const path_1 = __importDefault(require("path"));
9
- async function readFilesInDirectory(dir = "") {
9
+ async function readFilesInDirectory(dir = "", filterFunc) {
10
10
  let files = [];
11
11
  const items = await fs_extra_1.default.readdir(dir);
12
- for (const item of items) {
13
- const fullPath = path_1.default.join(dir, item);
14
- const stat = await fs_extra_1.default.stat(fullPath);
12
+ const filteredItems = items.map((i) => path_1.default.join(dir, i)).filter(filterFunc);
13
+ for (const item of filteredItems) {
14
+ const stat = await fs_extra_1.default.stat(item);
15
15
  if (stat.isDirectory()) {
16
- files = files.concat(await readFilesInDirectory(fullPath));
16
+ files = files.concat(await readFilesInDirectory(item, filterFunc));
17
17
  }
18
18
  else if (stat.isFile()) {
19
- const content = await fs_extra_1.default.readFile(fullPath, "utf-8");
20
- files.push({ filePath: fullPath, content });
19
+ const content = await fs_extra_1.default.readFile(item, "utf-8");
20
+ files.push({ filePath: item, content });
21
21
  }
22
22
  }
23
23
  return files;
@@ -28,14 +28,14 @@ function convertFileContentsToString(files = []) {
28
28
  files.forEach((file) => {
29
29
  prompt += `File Path: ${file.filePath}\n`;
30
30
  prompt += `File:\n`;
31
- prompt += `${file.content}\n\n ------ \n\n`;
31
+ prompt += `${file.content}\n\n------\n\n`;
32
32
  });
33
33
  return prompt;
34
34
  }
35
35
  exports.convertFileContentsToString = convertFileContentsToString;
36
- async function generatePromptFromDirectory(dir = "") {
36
+ async function generatePromptFromDirectory(dir = "", filterFunc) {
37
37
  try {
38
- const files = await readFilesInDirectory(dir);
38
+ const files = await readFilesInDirectory(dir, filterFunc);
39
39
  const prompt = convertFileContentsToString(files);
40
40
  return prompt;
41
41
  }
@@ -0,0 +1,10 @@
1
+ declare class TestFileService {
2
+ baseUrl: string;
3
+ constructor(port: number);
4
+ updateTest({ generatedCode, task, }: {
5
+ generatedCode: string;
6
+ task: string;
7
+ }): Promise<void>;
8
+ }
9
+ export default TestFileService;
10
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/file/client.ts"],"names":[],"mappings":"AAAA,cAAM,eAAe;IACnB,OAAO,EAAE,MAAM,CAAC;gBACJ,IAAI,EAAE,MAAM;IAIlB,UAAU,CAAC,EACf,aAAa,EACb,IAAI,GACL,EAAE;QACD,aAAa,EAAE,MAAM,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC;KACd;CAeF;AAED,eAAe,eAAe,CAAC"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class TestFileService {
4
+ baseUrl;
5
+ constructor(port) {
6
+ this.baseUrl = `http://localhost:${port}`;
7
+ }
8
+ async updateTest({ generatedCode, task, }) {
9
+ const resp = await fetch(`${this.baseUrl}/test`, {
10
+ method: "POST",
11
+ headers: {
12
+ "Content-Type": "application/json",
13
+ },
14
+ body: JSON.stringify({ generatedCode, task }),
15
+ });
16
+ if (!resp.ok) {
17
+ throw new Error(resp.statusText);
18
+ }
19
+ else {
20
+ console.log("Generated and updated test successfully");
21
+ }
22
+ }
23
+ }
24
+ exports.default = TestFileService;
@@ -0,0 +1,11 @@
1
+ export declare class FileService {
2
+ private filePath;
3
+ private port;
4
+ constructor({ port }: {
5
+ port: number;
6
+ });
7
+ setFilePath(filePath: string): void;
8
+ startFileService(): Promise<number>;
9
+ }
10
+ export declare function startFileService(): Promise<void>;
11
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/file/server.ts"],"names":[],"mappings":"AAOA,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,IAAI,CAAa;gBACb,EAAE,IAAI,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;IAGtC,WAAW,CAAC,QAAQ,EAAE,MAAM;IAGtB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;CAmC1C;AAED,wBAAsB,gBAAgB,kBAAK"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startFileService = exports.FileService = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const web_1 = require("../bin/utils/platform/web");
11
+ const string_1 = require("../utils/string");
12
+ class FileService {
13
+ filePath = "";
14
+ port = 0;
15
+ constructor({ port }) {
16
+ this.port = port;
17
+ }
18
+ setFilePath(filePath) {
19
+ this.filePath = filePath;
20
+ }
21
+ async startFileService() {
22
+ const app = (0, express_1.default)();
23
+ app.use(express_1.default.json());
24
+ app.post("/test", (req, res) => {
25
+ const { generatedCode, task } = req.body;
26
+ try {
27
+ const testFilePath = path_1.default.resolve(process.cwd(), this.filePath);
28
+ if (testFilePath) {
29
+ const testFile = fs_1.default.readFileSync(testFilePath, "utf-8");
30
+ const jsComments = (0, string_1.convertTextToJsComments)(task);
31
+ const updatedTestFile = testFile.replace(/await createTest\([\s\S]*?\);\n/, jsComments + "\n" + generatedCode);
32
+ const importStatement = `import { test, expect } from "@playwright/test";`;
33
+ fs_1.default.writeFileSync(testFilePath, importStatement + "\n" + updatedTestFile, "utf-8");
34
+ (0, web_1.lintErrors)(testFilePath);
35
+ return res.send({ success: true });
36
+ }
37
+ }
38
+ catch (e) {
39
+ console.error(e);
40
+ }
41
+ return res.send({ success: false });
42
+ });
43
+ return new Promise((resolve) => {
44
+ app.listen(this.port, () => resolve(this.port));
45
+ });
46
+ }
47
+ }
48
+ exports.FileService = FileService;
49
+ async function startFileService() { }
50
+ exports.startFileService = startFileService;
@@ -0,0 +1,3 @@
1
+ import { Page } from "playwright";
2
+ export declare function createTest(task: string, page: Page, test: any): Promise<void>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAKlC,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,iBAanE"}
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createTest = void 0;
7
+ const agent_1 = require("./agent");
8
+ const client_1 = __importDefault(require("./file/client"));
9
+ async function createTest(task, page, test) {
10
+ const port = process.env.APP_PORT || 3030;
11
+ const fileService = new client_1.default(Number(port));
12
+ test.setTimeout(900000);
13
+ const code = await (0, agent_1.browsingAgent)(task, page, {
14
+ htmlSanitize: {
15
+ disallowedStrings: ["v-data-table__td v-data-table-column--align-start"],
16
+ },
17
+ });
18
+ await fileService.updateTest({
19
+ task,
20
+ generatedCode: code,
21
+ });
22
+ }
23
+ exports.createTest = createTest;
@@ -0,0 +1,25 @@
1
+ import OpenAI from "openai";
2
+ import { Page } from "playwright";
3
+ export type FileContent = {
4
+ filePath: string;
5
+ content: string;
6
+ };
7
+ export type TestGenConfig = {
8
+ dir?: string;
9
+ specPath?: string;
10
+ scenarios: Scenario[];
11
+ };
12
+ export type Scenario = {
13
+ name: string;
14
+ steps: string[];
15
+ assert: string;
16
+ };
17
+ export type PlaywrightActionGenerator = (page: Page) => Action;
18
+ export type ActionSchema = OpenAI.Chat.Completions.ChatCompletionTool;
19
+ export type Action = {
20
+ name: string;
21
+ schema: ActionSchema;
22
+ execute: (args: Record<string, any>) => Promise<void>;
23
+ template: (args: Record<string, any>) => string;
24
+ };
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;AAE/D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC;AAEtE,MAAM,MAAM,MAAM,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,MAAM,CAAC;CACjD,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function cmd(command: string[], options: {
2
+ env?: Record<string, string>;
3
+ }): Promise<string | number>;
4
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAGA,wBAAgB,GAAG,CACjB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,CAe1B"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cmd = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const process_1 = __importDefault(require("process"));
9
+ function cmd(command, options) {
10
+ return new Promise((resolveFunc) => {
11
+ let p = (0, child_process_1.spawn)(command[0], command.slice(1), {
12
+ env: { ...process_1.default.env, ...options.env },
13
+ });
14
+ p.stdout.on("data", (x) => {
15
+ process_1.default.stdout.write(x.toString());
16
+ });
17
+ p.stderr.on("data", (x) => {
18
+ process_1.default.stderr.write(x.toString());
19
+ });
20
+ p.on("exit", (code) => {
21
+ resolveFunc(code);
22
+ });
23
+ });
24
+ }
25
+ exports.cmd = cmd;
@@ -0,0 +1,2 @@
1
+ export declare function findFileRecursively(directory: string, fileName: string): string | undefined;
2
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/utils/file.ts"],"names":[],"mappings":"AAGA,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAgBpB"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findFileRecursively = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function findFileRecursively(directory, fileName) {
10
+ const files = fs_1.default.readdirSync(directory);
11
+ for (const file of files) {
12
+ const fullPath = path_1.default.join(directory, file);
13
+ const stat = fs_1.default.statSync(fullPath);
14
+ if (stat.isDirectory()) {
15
+ const result = findFileRecursively(fullPath, fileName);
16
+ if (result) {
17
+ return result;
18
+ }
19
+ }
20
+ else if (file === fileName) {
21
+ return fullPath;
22
+ }
23
+ }
24
+ }
25
+ exports.findFileRecursively = findFileRecursively;
@@ -0,0 +1,4 @@
1
+ export declare const sanitizeHtml: (str: string, options?: {
2
+ disallowedStrings?: string[];
3
+ }) => string;
4
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/utils/html.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,QAClB,MAAM,YACF;IACP,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,WAuBF,CAAC"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sanitizeHtml = void 0;
7
+ const sanitize_html_1 = __importDefault(require("sanitize-html"));
8
+ const sanitizeHtml = (str, options = {}) => {
9
+ let sanitizedStr = (0, sanitize_html_1.default)(str, {
10
+ allowedTags: sanitize_html_1.default.defaults.allowedTags.concat([
11
+ "button",
12
+ "form",
13
+ "img",
14
+ "input",
15
+ "select",
16
+ "textarea",
17
+ "div",
18
+ "h1",
19
+ "h2",
20
+ "h3",
21
+ ]),
22
+ allowedAttributes: false,
23
+ });
24
+ if (options.disallowedStrings) {
25
+ options.disallowedStrings.forEach((s) => {
26
+ sanitizedStr = sanitizedStr.replaceAll(s, "");
27
+ });
28
+ }
29
+ return sanitizedStr;
30
+ };
31
+ exports.sanitizeHtml = sanitizeHtml;
@@ -0,0 +1,2 @@
1
+ export declare function convertTextToJsComments(text: string): string;
2
+ //# sourceMappingURL=string.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string.d.ts","sourceRoot":"","sources":["../../src/utils/string.ts"],"names":[],"mappings":"AAAA,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,UAInD"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertTextToJsComments = void 0;
4
+ function convertTextToJsComments(text) {
5
+ const lines = text.split("\n");
6
+ const commentedLines = lines.map((line) => `// ${line}`);
7
+ return commentedLines.join("\n");
8
+ }
9
+ exports.convertTextToJsComments = convertTextToJsComments;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.7.3",
3
+ "version": "0.8.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -15,22 +15,30 @@
15
15
  },
16
16
  "author": "Empirical Team <hey@empirical.run>",
17
17
  "dependencies": {
18
+ "@types/sanitize-html": "^2.11.0",
18
19
  "commander": "^12.1.0",
20
+ "detect-port": "^1.6.1",
19
21
  "dotenv": "^16.4.5",
20
22
  "eslint": "^8.57.0",
23
+ "express": "^4.19.2",
21
24
  "fs-extra": "^11.2.0",
22
25
  "google-auth-library": "^9.10.0",
23
26
  "google-spreadsheet": "^4.1.2",
27
+ "ignore": "^5.3.1",
24
28
  "langfuse": "^3.11.2",
25
29
  "langfuse-core": "^3.11.2",
26
30
  "openai": "^4.47.2",
27
31
  "picocolors": "^1.0.1",
32
+ "playwright": "^1.44.1",
28
33
  "prettier": "^3.2.5",
34
+ "sanitize-html": "^2.13.0",
29
35
  "slugify": "^1.6.6",
30
36
  "typescript": "^5.3.3",
31
37
  "yaml": "^2.4.2"
32
38
  },
33
39
  "devDependencies": {
40
+ "@types/detect-port": "^1.3.5",
41
+ "@types/express": "^4.17.21",
34
42
  "@types/fs-extra": "^11.0.4"
35
43
  },
36
44
  "scripts": {
@@ -1,15 +0,0 @@
1
- export type FileContent = {
2
- filePath: string;
3
- content: string;
4
- };
5
- export type TestGenConfig = {
6
- dir?: string;
7
- specPath?: string;
8
- scenarios: Scenario[];
9
- };
10
- export type Scenario = {
11
- name: string;
12
- steps: string[];
13
- assert: string;
14
- };
15
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC"}
File without changes