PMFlow/frontend/playwright-report/feishu-web/data/537e29afbd4d36736f6058ce967...

8.6 KiB

Instructions

  • Following Playwright test failed.
  • Explain why, be concise, respect Playwright best practices.
  • Provide a snippet of code with the fix, if possible.

Test info

  • Name: feishu-project-full-flow.spec.ts >> real Feishu web full project-creation flow with draft revisions and assignment
  • Location: e2e/feishu-project-full-flow.spec.ts:785:1

Error details

Error: initial draft work item timed out after 240000ms. Last value: null

Test source

  389 |   }
  390 | 
  391 |   return cloneRoot;
  392 | }
  393 | 
  394 | async function isContextUsable(
  395 |   context: Awaited<ReturnType<typeof chromium.launchPersistentContext>>,
  396 | ): Promise<boolean> {
  397 |   try {
  398 |     const page = context.pages()[0] ?? (await context.newPage());
  399 |     await page.waitForTimeout(250);
  400 |     return !page.isClosed();
  401 |   } catch {
  402 |     return false;
  403 |   }
  404 | }
  405 | 
  406 | async function launchFeishuContext(config: RuntimeConfig): Promise<LaunchOutcome> {
  407 |   const launchOptions = {
  408 |     args: config.profileDirectory ? [`--profile-directory=${config.profileDirectory}`] : [],
  409 |     executablePath: config.browserExecutable,
  410 |     headless: config.headless,
  411 |     slowMo: config.slowMoMs,
  412 |     viewport: { width: 1440, height: 960 },
  413 |   } as const;
  414 | 
  415 |   try {
  416 |     const context = await chromium.launchPersistentContext(config.userDataDir, launchOptions);
  417 |     if (!(await isContextUsable(context))) {
  418 |       await context.close().catch(() => undefined);
  419 |       const clonedUserDataDir = cloneChromeProfile(config);
  420 |       const clonedContext = await chromium.launchPersistentContext(clonedUserDataDir, launchOptions);
  421 |       return {
  422 |         context: clonedContext,
  423 |         launchMode: "cloned_profile",
  424 |         userDataDir: clonedUserDataDir,
  425 |       };
  426 |     }
  427 |     return {
  428 |       context,
  429 |       launchMode: "direct",
  430 |       userDataDir: config.userDataDir,
  431 |     };
  432 |   } catch (error) {
  433 |     const message = String(error instanceof Error ? error.message : error);
  434 |     if (!message.includes("ProcessSingleton")) {
  435 |       throw error;
  436 |     }
  437 | 
  438 |     const clonedUserDataDir = cloneChromeProfile(config);
  439 |     const context = await chromium.launchPersistentContext(clonedUserDataDir, launchOptions);
  440 |     return {
  441 |       context,
  442 |       launchMode: "cloned_profile",
  443 |       userDataDir: clonedUserDataDir,
  444 |     };
  445 |   }
  446 | }
  447 | 
  448 | async function captureScreenshot(page: Page, testInfo: TestInfo, name: string): Promise<void> {
  449 |   const targetPath = testInfo.outputPath(`${name}.png`);
  450 |   await page.screenshot({ fullPage: true, path: targetPath });
  451 |   await testInfo.attach(name, {
  452 |     body: await fs.promises.readFile(targetPath),
  453 |     contentType: "image/png",
  454 |   });
  455 | }
  456 | 
  457 | async function runDbProbe<T>(config: RuntimeConfig, payload: Record<string, unknown>): Promise<T> {
  458 |   const stdout = execFileSync(
  459 |     config.pythonPath,
  460 |     ["-c", DB_HELPER, PROJECT_ROOT],
  461 |     {
  462 |       cwd: PROJECT_ROOT,
  463 |       env: process.env,
  464 |       input: JSON.stringify(payload),
  465 |       maxBuffer: 10 * 1024 * 1024,
  466 |       stdio: ["pipe", "pipe", "pipe"],
  467 |       encoding: "utf-8",
  468 |     },
  469 |   );
  470 |   return JSON.parse(stdout.trim()) as T;
  471 | }
  472 | 
  473 | async function pollUntil<T>(
  474 |   label: string,
  475 |   timeoutMs: number,
  476 |   pollIntervalMs: number,
  477 |   probe: () => Promise<T>,
  478 |   predicate: (value: T) => boolean,
  479 | ): Promise<T> {
  480 |   const deadline = Date.now() + timeoutMs;
  481 |   let lastValue: T | undefined;
  482 |   while (Date.now() < deadline) {
  483 |     lastValue = await probe();
  484 |     if (predicate(lastValue)) {
  485 |       return lastValue;
  486 |     }
  487 |     await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
  488 |   }
> 489 |   throw new Error(`${label} timed out after ${timeoutMs}ms. Last value: ${JSON.stringify(lastValue, null, 2)}`);
      |         ^ Error: initial draft work item timed out after 240000ms. Last value: null
  490 | }
  491 | 
  492 | async function pickBottomComposerOrNull(page: Page): Promise<ElementHandle<HTMLElement> | null> {
  493 |   const candidates = await page.locator('textarea, [contenteditable="true"]').elementHandles();
  494 |   let bestHandle: ElementHandle<HTMLElement> | null = null;
  495 |   let bestBottom = -1;
  496 |   for (const candidate of candidates) {
  497 |     const asHtml = candidate as ElementHandle<HTMLElement>;
  498 |     const box = await asHtml.boundingBox();
  499 |     if (!box || box.width < 60 || box.height < 16) {
  500 |       continue;
  501 |     }
  502 |     const visible = await asHtml.isVisible();
  503 |     if (!visible) {
  504 |       continue;
  505 |     }
  506 |     const bottom = box.y + box.height;
  507 |     if (bottom > bestBottom) {
  508 |       bestBottom = bottom;
  509 |       bestHandle = asHtml;
  510 |     }
  511 |   }
  512 |   return bestHandle;
  513 | }
  514 | 
  515 | async function pickBottomComposer(page: Page): Promise<ElementHandle<HTMLElement>> {
  516 |   const bestHandle = await pickBottomComposerOrNull(page);
  517 |   if (!bestHandle) {
  518 |     throw new Error("No visible Feishu message composer was found.");
  519 |   }
  520 |   return bestHandle;
  521 | }
  522 | 
  523 | async function waitForVisibleComposer(page: Page, timeoutMs = 12_000): Promise<boolean> {
  524 |   const deadline = Date.now() + timeoutMs;
  525 |   while (Date.now() < deadline) {
  526 |     if (await pickBottomComposerOrNull(page)) {
  527 |       return true;
  528 |     }
  529 |     await page.waitForTimeout(500);
  530 |   }
  531 |   return false;
  532 | }
  533 | 
  534 | async function sendChatMessage(page: Page, message: string, sendShortcut: string): Promise<void> {
  535 |   const composer = await pickBottomComposer(page);
  536 |   const isTextArea = await composer.evaluate((node) => node.tagName.toLowerCase() === "textarea");
  537 |   await composer.click({ force: true });
  538 |   const selectAllShortcut = process.platform === "darwin" ? "Meta+A" : "Control+A";
  539 |   await page.keyboard.press(selectAllShortcut).catch(() => undefined);
  540 |   await page.keyboard.press("Backspace").catch(() => undefined);
  541 |   if (isTextArea) {
  542 |     await composer.fill(message);
  543 |   } else {
  544 |     await page.keyboard.type(message, { delay: 18 });
  545 |   }
  546 |   await page.keyboard.press(sendShortcut);
  547 | }
  548 | 
  549 | async function dismissSoftOverlays(page: Page): Promise<void> {
  550 |   await page.keyboard.press("Escape").catch(() => undefined);
  551 |   const softButtons = ["知道了", "我知道了", "稍后", "下次再说"];
  552 |   for (const label of softButtons) {
  553 |     const button = page.getByRole("button", { name: label }).first();
  554 |     if (await button.isVisible().catch(() => false)) {
  555 |       await button.click().catch(() => undefined);
  556 |     }
  557 |   }
  558 | }
  559 | 
  560 | async function assertLoggedIn(page: Page, testInfo: TestInfo): Promise<void> {
  561 |   const loginSignals = [
  562 |     "text=扫码登录",
  563 |     "text=密码登录",
  564 |     "text=Log In With QR Code",
  565 |     "text=Switch to Lark to log in",
  566 |     "text=欢迎使用 Lark",
  567 |     "text=切换至飞书登录",
  568 |     "input[type='password']",
  569 |   ];
  570 |   for (const selector of loginSignals) {
  571 |     const matched = await page.locator(selector).first().isVisible().catch(() => false);
  572 |     if (matched) {
  573 |       await captureScreenshot(page, testInfo, "feishu-login-required");
  574 |       throw new Error("Feishu web is not logged in. Please provide a logged-in Chrome profile via FEISHU_USER_DATA_DIR / FEISHU_PROFILE_DIRECTORY.");
  575 |     }
  576 |   }
  577 | }
  578 | 
  579 | async function readChatCandidates(page: Page, desiredName?: string): Promise<FeishuChatCandidate[]> {
  580 |   return page.evaluate((inputDesiredName) => {
  581 |     const normalizedDesiredName = String(inputDesiredName || "").trim().toLowerCase();
  582 |     const feedState = (
  583 |       globalThis as typeof globalThis & {
  584 |         __feedStore?: {
  585 |           getState?: () => {
  586 |             previews?: Record<string, Record<string, unknown>>;
  587 |           };
  588 |         };
  589 |       }