testContext.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __export = (target, all) => {
  9. for (var name in all)
  10. __defProp(target, name, { get: all[name], enumerable: true });
  11. };
  12. var __copyProps = (to, from, except, desc) => {
  13. if (from && typeof from === "object" || typeof from === "function") {
  14. for (let key of __getOwnPropNames(from))
  15. if (!__hasOwnProp.call(to, key) && key !== except)
  16. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  17. }
  18. return to;
  19. };
  20. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  21. // If the importer is in node compatibility mode or this is not an ESM
  22. // file that has been converted to a CommonJS file using a Babel-
  23. // compatible transform (i.e. "__esModule" has not been set), then set
  24. // "default" to the CommonJS "module.exports" for node compatibility.
  25. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  26. mod
  27. ));
  28. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  29. var testContext_exports = {};
  30. __export(testContext_exports, {
  31. GeneratorJournal: () => GeneratorJournal,
  32. TestContext: () => TestContext,
  33. createScreen: () => createScreen
  34. });
  35. module.exports = __toCommonJS(testContext_exports);
  36. var import_fs = __toESM(require("fs"));
  37. var import_os = __toESM(require("os"));
  38. var import_path = __toESM(require("path"));
  39. var import_utils = require("playwright-core/lib/utils");
  40. var import_base = require("../../reporters/base");
  41. var import_list = __toESM(require("../../reporters/list"));
  42. var import_streams = require("./streams");
  43. var import_util = require("../../util");
  44. var import_testRunner = require("../../runner/testRunner");
  45. var import_seed = require("./seed");
  46. var import_exports = require("../sdk/exports");
  47. var import_configLoader = require("../../common/configLoader");
  48. var import_response = require("../browser/response");
  49. var import_log = require("../log");
  50. class GeneratorJournal {
  51. constructor(rootPath, plan, seed) {
  52. this._rootPath = rootPath;
  53. this._plan = plan;
  54. this._seed = seed;
  55. this._steps = [];
  56. }
  57. logStep(title, code) {
  58. if (title)
  59. this._steps.push({ title, code });
  60. }
  61. journal() {
  62. const result = [];
  63. result.push(`# Plan`);
  64. result.push(this._plan);
  65. result.push(`# Seed file: ${import_path.default.relative(this._rootPath, this._seed.file)}`);
  66. result.push("```ts");
  67. result.push(this._seed.content);
  68. result.push("```");
  69. result.push(`# Steps`);
  70. result.push(this._steps.map((step) => `### ${step.title}
  71. \`\`\`ts
  72. ${step.code}
  73. \`\`\``).join("\n\n"));
  74. result.push(bestPracticesMarkdown);
  75. return result.join("\n\n");
  76. }
  77. }
  78. class TestContext {
  79. constructor(clientInfo, configPath, options) {
  80. this._clientInfo = clientInfo;
  81. const rootPath = (0, import_exports.firstRootPath)(clientInfo);
  82. this._configLocation = (0, import_configLoader.resolveConfigLocation)(configPath || rootPath);
  83. this.rootPath = rootPath || this._configLocation.configDir;
  84. if (options?.headless !== void 0)
  85. this.computedHeaded = !options.headless;
  86. else
  87. this.computedHeaded = !process.env.CI && !(import_os.default.platform() === "linux" && !process.env.DISPLAY);
  88. }
  89. existingTestRunner() {
  90. return this._testRunnerAndScreen?.testRunner;
  91. }
  92. async _cleanupTestRunner() {
  93. if (!this._testRunnerAndScreen)
  94. return;
  95. await this._testRunnerAndScreen.testRunner.stopTests();
  96. this._testRunnerAndScreen.claimStdio();
  97. try {
  98. await this._testRunnerAndScreen.testRunner.runGlobalTeardown();
  99. } finally {
  100. this._testRunnerAndScreen.releaseStdio();
  101. this._testRunnerAndScreen = void 0;
  102. }
  103. }
  104. async createTestRunner() {
  105. await this._cleanupTestRunner();
  106. const testRunner = new import_testRunner.TestRunner(this._configLocation, {});
  107. await testRunner.initialize({});
  108. const testPaused = new import_utils.ManualPromise();
  109. const testRunnerAndScreen = {
  110. ...createScreen(),
  111. testRunner,
  112. waitForTestPaused: () => testPaused
  113. };
  114. this._testRunnerAndScreen = testRunnerAndScreen;
  115. testRunner.on(import_testRunner.TestRunnerEvent.TestPaused, (params) => {
  116. testRunnerAndScreen.sendMessageToPausedTest = params.sendMessage;
  117. testPaused.resolve();
  118. });
  119. return testRunnerAndScreen;
  120. }
  121. async getOrCreateSeedFile(seedFile, projectName) {
  122. const configDir = this._configLocation.configDir;
  123. const { testRunner } = await this.createTestRunner();
  124. const config = await testRunner.loadConfig();
  125. const project = (0, import_seed.seedProject)(config, projectName);
  126. if (!seedFile) {
  127. seedFile = await (0, import_seed.ensureSeedFile)(project);
  128. } else {
  129. const candidateFiles = [];
  130. const testDir = project.project.testDir;
  131. candidateFiles.push(import_path.default.resolve(testDir, seedFile));
  132. candidateFiles.push(import_path.default.resolve(configDir, seedFile));
  133. candidateFiles.push(import_path.default.resolve(this.rootPath, seedFile));
  134. let resolvedSeedFile;
  135. for (const candidateFile of candidateFiles) {
  136. if (await (0, import_util.fileExistsAsync)(candidateFile)) {
  137. resolvedSeedFile = candidateFile;
  138. break;
  139. }
  140. }
  141. if (!resolvedSeedFile)
  142. throw new Error("seed test not found.");
  143. seedFile = resolvedSeedFile;
  144. }
  145. const seedFileContent = await import_fs.default.promises.readFile(seedFile, "utf8");
  146. return {
  147. file: seedFile,
  148. content: seedFileContent,
  149. projectName: project.project.name
  150. };
  151. }
  152. async runSeedTest(seedFile, projectName) {
  153. const result = await this.runTestsWithGlobalSetupAndPossiblePause({
  154. headed: this.computedHeaded,
  155. locations: ["/" + (0, import_utils.escapeRegExp)(seedFile) + "/"],
  156. projects: [projectName],
  157. timeout: 0,
  158. workers: 1,
  159. pauseAtEnd: true,
  160. disableConfigReporters: true,
  161. failOnLoadErrors: true
  162. });
  163. if (result.status === "passed")
  164. result.output += "\nError: seed test not found.";
  165. else if (result.status !== "paused")
  166. result.output += "\nError while running the seed test.";
  167. return result;
  168. }
  169. async runTestsWithGlobalSetupAndPossiblePause(params) {
  170. const configDir = this._configLocation.configDir;
  171. const testRunnerAndScreen = await this.createTestRunner();
  172. const { testRunner, screen, claimStdio, releaseStdio } = testRunnerAndScreen;
  173. claimStdio();
  174. try {
  175. const setupReporter = new import_list.default({ configDir, screen, includeTestId: true });
  176. const { status: status2 } = await testRunner.runGlobalSetup([setupReporter]);
  177. if (status2 !== "passed")
  178. return { output: testRunnerAndScreen.output.join("\n"), status: status2 };
  179. } finally {
  180. releaseStdio();
  181. }
  182. let status = "passed";
  183. const cleanup = async () => {
  184. claimStdio();
  185. try {
  186. const result = await testRunner.runGlobalTeardown();
  187. if (status === "passed")
  188. status = result.status;
  189. } finally {
  190. releaseStdio();
  191. }
  192. };
  193. try {
  194. const reporter = new import_list.default({ configDir, screen, includeTestId: true });
  195. status = await Promise.race([
  196. testRunner.runTests(reporter, params).then((result) => result.status),
  197. testRunnerAndScreen.waitForTestPaused().then(() => "paused")
  198. ]);
  199. if (status === "paused") {
  200. const response = await testRunnerAndScreen.sendMessageToPausedTest({ request: { initialize: { clientInfo: this._clientInfo } } });
  201. if (response.error)
  202. throw new Error(response.error.message);
  203. testRunnerAndScreen.output.push(response.response.initialize.pausedMessage);
  204. return { output: testRunnerAndScreen.output.join("\n"), status };
  205. }
  206. } catch (e) {
  207. status = "failed";
  208. testRunnerAndScreen.output.push(String(e));
  209. await cleanup();
  210. return { output: testRunnerAndScreen.output.join("\n"), status };
  211. }
  212. await cleanup();
  213. return { output: testRunnerAndScreen.output.join("\n"), status };
  214. }
  215. async close() {
  216. await this._cleanupTestRunner().catch(import_log.logUnhandledError);
  217. }
  218. async sendMessageToPausedTest(request) {
  219. const sendMessage = this._testRunnerAndScreen?.sendMessageToPausedTest;
  220. if (!sendMessage)
  221. throw new Error("Must setup test before interacting with the page");
  222. const result = await sendMessage({ request });
  223. if (result.error)
  224. throw new Error(result.error.message);
  225. if (typeof request?.callTool?.arguments?.["intent"] === "string") {
  226. const response = (0, import_response.parseResponse)(result.response.callTool);
  227. if (response && !response.isError && response.code)
  228. this.generatorJournal?.logStep(request.callTool.arguments["intent"], response.code);
  229. }
  230. return result.response;
  231. }
  232. }
  233. function createScreen() {
  234. const output = [];
  235. const stdout = new import_streams.StringWriteStream(output, "stdout");
  236. const stderr = new import_streams.StringWriteStream(output, "stderr");
  237. const screen = {
  238. ...import_base.terminalScreen,
  239. isTTY: false,
  240. colors: import_utils.noColors,
  241. stdout,
  242. stderr
  243. };
  244. const originalStdoutWrite = process.stdout.write;
  245. const originalStderrWrite = process.stderr.write;
  246. const claimStdio = () => {
  247. process.stdout.write = (chunk) => {
  248. stdout.write(chunk);
  249. return true;
  250. };
  251. process.stderr.write = (chunk) => {
  252. stderr.write(chunk);
  253. return true;
  254. };
  255. };
  256. const releaseStdio = () => {
  257. process.stdout.write = originalStdoutWrite;
  258. process.stderr.write = originalStderrWrite;
  259. };
  260. return { screen, claimStdio, releaseStdio, output };
  261. }
  262. const bestPracticesMarkdown = `
  263. # Best practices
  264. - Do not improvise, do not add directives that were not asked for
  265. - Use clear, descriptive assertions to validate the expected behavior
  266. - Use reliable locators from this log
  267. - Use local variables for locators that are used multiple times
  268. - Use Playwright waiting assertions and best practices from this log
  269. - NEVER! use page.waitForLoadState()
  270. - NEVER! use page.waitForNavigation()
  271. - NEVER! use page.waitForTimeout()
  272. - NEVER! use page.evaluate()
  273. `;
  274. // Annotate the CommonJS export names for ESM import in node:
  275. 0 && (module.exports = {
  276. GeneratorJournal,
  277. TestContext,
  278. createScreen
  279. });