PMFlow/frontend/test-results/feishu-project-full-flow-r-.../error-context.md

223 lines
8.6 KiB
Markdown

# 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
```ts
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 | }
```