| ... | @@ -148,22 +148,58 @@ interface CdpEvaluationResult { | ... | @@ -148,22 +148,58 @@ interface CdpEvaluationResult { |
| 148 | }; | 148 | }; |
| 149 | } | 149 | } |
| 150 | | 150 | |
| 151 | const automationError = ( | 151 | export const PreviewAutomationSelectorKind = Schema.Literals([ |
| 152 | tag: | 152 | "focused-element", |
| 153 | | "PreviewAutomationExecutionError" | 153 | "selector", |
| 154 | | "PreviewAutomationInvalidSelectorError" | 154 | "locator", |
| 155 | | "PreviewAutomationResultTooLargeError" | 155 | ]); |
| 156 | | "PreviewAutomationTimeoutError" | 156 | export type PreviewAutomationSelectorKind = typeof PreviewAutomationSelectorKind.Type; |
| 157 | | "PreviewAutomationControlInterruptedError", | 157 | |
| 158 | message: string, | 158 | export const PreviewAutomationEvaluationDetailKind = Schema.Literals([ |
| 159 | detail?: unknown, | 159 | "exception-description", |
| 160 | ): Error & { detail?: unknown } => { | 160 | "exception-text", |
| 161 | const error = new Error(message) as Error & { detail?: unknown }; | 161 | "unknown", |
| 162 | error.name = tag; | 162 | ]); |
| 163 | if (detail !== undefined) error.detail = detail; | 163 | export type PreviewAutomationEvaluationDetailKind = |
| 164 | return error; | 164 | typeof PreviewAutomationEvaluationDetailKind.Type; |
| | 165 | |
| | 166 | const previewAutomationEvaluationDetail = (exceptionDetails: unknown) => { |
| | 167 | if (typeof exceptionDetails !== "object" || exceptionDetails === null) { |
| | 168 | return { detailKind: "unknown" as const }; |
| | 169 | } |
| | 170 | const details = exceptionDetails as Record<string, unknown>; |
| | 171 | const exception = details["exception"]; |
| | 172 | const description = |
| | 173 | typeof exception === "object" && |
| | 174 | exception !== null && |
| | 175 | typeof (exception as Record<string, unknown>)["description"] === "string" |
| | 176 | ? (exception as Record<string, unknown>)["description"] |
| | 177 | : undefined; |
| | 178 | if (typeof description === "string" && description.length > 0) { |
| | 179 | return { detailKind: "exception-description" as const, detail: description }; |
| | 180 | } |
| | 181 | const text = details["text"]; |
| | 182 | if (typeof text === "string" && text.length > 0) { |
| | 183 | return { detailKind: "exception-text" as const, detail: text }; |
| | 184 | } |
| | 185 | return { detailKind: "unknown" as const }; |
| 165 | }; | 186 | }; |
| 166 | | 187 | |
| | 188 | const previewAutomationTargetLabel = ( |
| | 189 | selectorKind: PreviewAutomationSelectorKind, |
| | 190 | selectorLength?: number, |
| | 191 | ) => |
| | 192 | selectorKind === "focused-element" |
| | 193 | ? "the focused element" |
| | 194 | : `${selectorKind} (${selectorLength ?? 0} characters)`; |
| | 195 | |
| | 196 | interface PreviewOperationContext { |
| | 197 | readonly operation: string; |
| | 198 | readonly tabId?: string; |
| | 199 | readonly webContentsId?: number; |
| | 200 | readonly artifactPath?: string; |
| | 201 | } |
| | 202 | |
| 167 | const normalizeCaptureRect = (value: unknown): PreviewAnnotationRect | null => { | 203 | const normalizeCaptureRect = (value: unknown): PreviewAnnotationRect | null => { |
| 168 | if (typeof value !== "object" || value === null) return null; | 204 | if (typeof value !== "object" || value === null) return null; |
| 169 | const rect = value as Record<string, unknown>; | 205 | const rect = value as Record<string, unknown>; |
| ... | @@ -194,6 +230,7 @@ const normalizeCaptureRect = (value: unknown): PreviewAnnotationRect | null => { | ... | @@ -194,6 +230,7 @@ const normalizeCaptureRect = (value: unknown): PreviewAnnotationRect | null => { |
| 194 | }; | 230 | }; |
| 195 | | 231 | |
| 196 | const captureAnnotationScreenshot = ( | 232 | const captureAnnotationScreenshot = ( |
| | 233 | tabId: string, |
| 197 | wc: Electron.WebContents, | 234 | wc: Electron.WebContents, |
| 198 | cropRect: PreviewAnnotationRect | null, | 235 | cropRect: PreviewAnnotationRect | null, |
| 199 | ): Effect.Effect<PreviewAnnotationPayload["screenshot"], PreviewManagerError> => | 236 | ): Effect.Effect<PreviewAnnotationPayload["screenshot"], PreviewManagerError> => |
| ... | @@ -209,7 +246,13 @@ const captureAnnotationScreenshot = ( | ... | @@ -209,7 +246,13 @@ const captureAnnotationScreenshot = ( |
| 209 | } | 246 | } |
| 210 | : undefined, | 247 | : undefined, |
| 211 | ), | 248 | ), |
| 212 | catch: (cause) => new PreviewManagerError({ operation: "captureAnnotationScreenshot", cause }), | 249 | catch: (cause) => |
| | 250 | new PreviewOperationError({ |
| | 251 | operation: "captureAnnotationScreenshot", |
| | 252 | tabId, |
| | 253 | webContentsId: wc.id, |
| | 254 | cause, |
| | 255 | }), |
| 213 | }).pipe( | 256 | }).pipe( |
| 214 | Effect.map((image) => { | 257 | Effect.map((image) => { |
| 215 | const size = image.getSize(); | 258 | const size = image.getSize(); |
| ... | @@ -341,15 +384,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -341,15 +384,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 341 | const runFork = Effect.runForkWith(context); | 384 | const runFork = Effect.runForkWith(context); |
| 342 | const resolvedArtifactDirectory = path.resolve(artifactDirectory); | 385 | const resolvedArtifactDirectory = path.resolve(artifactDirectory); |
| 343 | const playwrightInstallExpression = yield* Effect.cached( | 386 | const playwrightInstallExpression = yield* Effect.cached( |
| 344 | playwrightInjectedRuntimeInstallExpression().pipe( | 387 | playwrightInjectedRuntimeInstallExpression(), |
| 345 | Effect.mapError( | | |
| 346 | (cause) => | | |
| 347 | new PreviewManagerError({ | | |
| 348 | operation: "ensurePlaywrightInjected", | | |
| 349 | cause, | | |
| 350 | }), | | |
| 351 | ), | | |
| 352 | ), | | |
| 353 | ); | 388 | ); |
| 354 | | 389 | |
| 355 | const annotationThemeRef = yield* Ref.make(DEFAULT_ANNOTATION_THEME); | 390 | const annotationThemeRef = yield* Ref.make(DEFAULT_ANNOTATION_THEME); |
| ... | @@ -377,16 +412,25 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -377,16 +412,25 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 377 | const pointerSequenceRef = yield* Ref.make(0); | 412 | const pointerSequenceRef = yield* Ref.make(0); |
| 378 | const recordingTabIdRef = yield* Ref.make<Option.Option<string>>(Option.none()); | 413 | const recordingTabIdRef = yield* Ref.make<Option.Option<string>>(Option.none()); |
| 379 | | 414 | |
| 380 | const fail = (operation: string, cause: unknown): PreviewManagerError => | 415 | const attempt = <A>(errorContext: PreviewOperationContext, evaluate: () => A) => |
| 381 | new PreviewManagerError({ operation, cause }); | 416 | Effect.try({ |
| 382 | const attempt = <A>(operation: string, evaluate: () => A) => | 417 | try: evaluate, |
| 383 | Effect.try({ try: evaluate, catch: (cause) => fail(operation, cause) }); | 418 | catch: (cause) => new PreviewOperationError({ ...errorContext, cause }), |
| 384 | const attemptPromise = <A>(operation: string, evaluate: () => PromiseLike<A>) => | 419 | }); |
| 385 | Effect.tryPromise({ try: evaluate, catch: (cause) => fail(operation, cause) }); | 420 | const attemptPromise = <A>( |
| | 421 | errorContext: PreviewOperationContext, |
| | 422 | evaluate: () => PromiseLike<A>, |
| | 423 | ) => |
| | 424 | Effect.tryPromise({ |
| | 425 | try: evaluate, |
| | 426 | catch: (cause) => new PreviewOperationError({ ...errorContext, cause }), |
| | 427 | }); |
| 386 | const currentIso = DateTime.now.pipe(Effect.map(DateTime.formatIso)); | 428 | const currentIso = DateTime.now.pipe(Effect.map(DateTime.formatIso)); |
| 387 | const currentMillis = Clock.currentTimeMillis; | 429 | const currentMillis = Clock.currentTimeMillis; |
| 388 | const encodeJson = (operation: string, value: unknown) => | 430 | const encodeJson = (errorContext: PreviewOperationContext, value: unknown) => |
| 389 | encodeUnknownJson(value).pipe(Effect.mapError((cause) => fail(operation, cause))); | 431 | encodeUnknownJson(value).pipe( |
| | 432 | Effect.mapError((cause) => new PreviewOperationError({ ...errorContext, cause })), |
| | 433 | ); |
| 390 | const nextCounter = (ref: Ref.Ref<number>) => | 434 | const nextCounter = (ref: Ref.Ref<number>) => |
| 391 | Ref.modify(ref, (value) => [value, value + 1] as const); | 435 | Ref.modify(ref, (value) => [value, value + 1] as const); |
| 392 | const replaceMap = <K, V>( | 436 | const replaceMap = <K, V>( |
| ... | @@ -432,23 +476,23 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -432,23 +476,23 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 432 | const tabs = yield* SynchronizedRef.get(tabsRef); | 476 | const tabs = yield* SynchronizedRef.get(tabsRef); |
| 433 | const tab = tabs.get(tabId); | 477 | const tab = tabs.get(tabId); |
| 434 | if (!tab) { | 478 | if (!tab) { |
| 435 | return yield* fail("requireWebContents", new PreviewTabNotFoundError({ tabId })); | 479 | return yield* new PreviewTabNotFoundError({ tabId }); |
| 436 | } | 480 | } |
| 437 | if (tab.webContentsId == null) { | 481 | if (tab.webContentsId == null) { |
| 438 | return yield* fail("requireWebContents", new PreviewWebviewNotInitializedError({ tabId })); | 482 | return yield* new PreviewWebviewNotInitializedError({ tabId }); |
| 439 | } | 483 | } |
| 440 | const wc = webContents.fromId(tab.webContentsId); | 484 | const wc = webContents.fromId(tab.webContentsId); |
| 441 | if (!wc) { | 485 | if (!wc) { |
| 442 | return yield* fail( | 486 | return yield* new PreviewWebContentsNotFoundError({ |
| 443 | "requireWebContents", | 487 | tabId, |
| 444 | new PreviewWebContentsNotFoundError({ tabId, webContentsId: tab.webContentsId }), | 488 | webContentsId: tab.webContentsId, |
| 445 | ); | 489 | }); |
| 446 | } | 490 | } |
| 447 | return wc; | 491 | return wc; |
| 448 | }); | 492 | }); |
| 449 | | 493 | |
| 450 | const resolveArtifactPath = (artifactPath: string) => | 494 | const resolveArtifactPath = (artifactPath: string) => |
| 451 | attempt("resolveArtifactPath", () => { | 495 | attempt({ operation: "resolveArtifactPath", artifactPath }, () => { |
| 452 | const resolvedPath = path.resolve(artifactPath); | 496 | const resolvedPath = path.resolve(artifactPath); |
| 453 | const relativePath = path.relative(resolvedArtifactDirectory, resolvedPath); | 497 | const relativePath = path.relative(resolvedArtifactDirectory, resolvedPath); |
| 454 | if ( | 498 | if ( |
| ... | @@ -464,10 +508,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -464,10 +508,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 464 | Effect.flatMap((resolvedPath) => | 508 | Effect.flatMap((resolvedPath) => |
| 465 | resolvedPath === null | 509 | resolvedPath === null |
| 466 | ? Effect.fail( | 510 | ? Effect.fail( |
| 467 | fail( | 511 | new PreviewArtifactPathOutsideDirectoryError({ |
| 468 | "resolveArtifactPath", | 512 | artifactPath, |
| 469 | new Error("Preview artifact path is outside the configured artifact directory."), | 513 | artifactDirectory: resolvedArtifactDirectory, |
| 470 | ), | 514 | }), |
| 471 | ) | 515 | ) |
| 472 | : Effect.succeed(resolvedPath), | 516 | : Effect.succeed(resolvedPath), |
| 473 | ), | 517 | ), |
| ... | @@ -636,129 +680,137 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -636,129 +680,137 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 636 | const ensureControlSession = Effect.fn("PreviewManager.ensureControlSession")(function* ( | 680 | const ensureControlSession = Effect.fn("PreviewManager.ensureControlSession")(function* ( |
| 637 | wc: Electron.WebContents, | 681 | wc: Electron.WebContents, |
| 638 | ) { | 682 | ) { |
| 639 | return yield* SynchronizedRef.modifyEffect(controlSessionsRef, (sessions) => { | 683 | return yield* SynchronizedRef.modifyEffect( |
| 640 | const existing = sessions.get(wc.id); | 684 | controlSessionsRef, |
| 641 | if (existing) return Effect.succeed([existing, sessions] as const); | 685 | ( |
| 642 | if (wc.isDevToolsOpened()) { | 686 | sessions, |
| 643 | return Effect.fail( | 687 | ): Effect.Effect< |
| 644 | fail( | 688 | readonly [BrowserControlSession, ReadonlyMap<number, BrowserControlSession>], |
| 645 | "ensureControlSession", | 689 | PreviewManagerError |
| 646 | automationError( | 690 | > => { |
| 647 | "PreviewAutomationExecutionError", | 691 | const existing = sessions.get(wc.id); |
| 648 | "Close preview DevTools before using agent browser control.", | 692 | if (existing) return Effect.succeed([existing, sessions] as const); |
| 649 | ), | 693 | if (wc.isDevToolsOpened()) { |
| 650 | ), | 694 | return Effect.fail( |
| 651 | ); | 695 | new PreviewAutomationDevToolsOpenError({ |
| 652 | } | 696 | webContentsId: wc.id, |
| 653 | if (wc.debugger.isAttached()) { | | |
| 654 | return Effect.fail( | | |
| 655 | fail( | | |
| 656 | "ensureControlSession", | | |
| 657 | automationError( | | |
| 658 | "PreviewAutomationExecutionError", | | |
| 659 | "Preview control cannot attach because another debugger owns this page.", | | |
| 660 | ), | | |
| 661 | ), | | |
| 662 | ); | | |
| 663 | } | | |
| 664 | const createControlSession = Effect.fn("PreviewManager.createControlSession")(function* () { | | |
| 665 | const semaphore = yield* Semaphore.make(1); | | |
| 666 | const scope = yield* Scope.fork(parentScope, "sequential"); | | |
| 667 | const handleDebuggerMessage = Effect.fn("PreviewManager.handleDebuggerMessage")(function* ( | | |
| 668 | method: string, | | |
| 669 | params: Record<string, unknown>, | | |
| 670 | ) { | | |
| 671 | if (method === "Page.screencastFrame") { | | |
| 672 | const sessionId = params["sessionId"]; | | |
| 673 | if (typeof sessionId === "number") { | | |
| 674 | yield* attemptPromise("ackScreencastFrame", () => | | |
| 675 | wc.debugger.sendCommand("Page.screencastFrameAck", { sessionId }), | | |
| 676 | ).pipe(Effect.ignore); | | |
| 677 | } | | |
| 678 | const tabId = yield* tabIdForWebContents(wc.id); | | |
| 679 | const metadata = | | |
| 680 | typeof params["metadata"] === "object" && params["metadata"] !== null | | |
| 681 | ? (params["metadata"] as Record<string, unknown>) | | |
| 682 | : {}; | | |
| 683 | if (tabId && typeof params["data"] === "string") { | | |
| 684 | const receivedAt = yield* currentIso; | | |
| 685 | const listeners = yield* Ref.get(recordingFrameListenersRef); | | |
| 686 | const frame: DesktopPreviewRecordingFrame = { | | |
| 687 | tabId, | | |
| 688 | data: params["data"], | | |
| 689 | width: typeof metadata["deviceWidth"] === "number" ? metadata["deviceWidth"] : 0, | | |
| 690 | height: typeof metadata["deviceHeight"] === "number" ? metadata["deviceHeight"] : 0, | | |
| 691 | receivedAt, | | |
| 692 | }; | | |
| 693 | yield* Effect.forEach( | | |
| 694 | listeners, | | |
| 695 | (listener) => Effect.sync(() => listener(frame)).pipe(Effect.ignore), | | |
| 696 | { discard: true }, | | |
| 697 | ); | | |
| 698 | } | | |
| 699 | } | | |
| 700 | yield* captureDiagnosticMessage(wc.id, method, params); | | |
| 701 | }); | | |
| 702 | const onMessage: BrowserControlSession["onMessage"] = (_event, method, params) => { | | |
| 703 | runFork(handleDebuggerMessage(method, params)); | | |
| 704 | }; | | |
| 705 | yield* Scope.addFinalizer( | | |
| 706 | scope, | | |
| 707 | Effect.all( | | |
| 708 | [ | | |
| 709 | Ref.update(diagnosticsRef, (diagnostics) => | | |
| 710 | replaceMap(diagnostics, (copy) => { | | |
| 711 | copy.delete(wc.id); | | |
| 712 | }), | | |
| 713 | ), | | |
| 714 | attempt("detachControlSession", () => { | | |
| 715 | wc.debugger.off("message", onMessage); | | |
| 716 | if (wc.debugger.isAttached()) wc.debugger.detach(); | | |
| 717 | }).pipe(Effect.ignore), | | |
| 718 | ], | | |
| 719 | { discard: true }, | | |
| 720 | ), | | |
| 721 | ); | | |
| 722 | const control: BrowserControlSession = { | | |
| 723 | webContentsId: wc.id, | | |
| 724 | semaphore, | | |
| 725 | scope, | | |
| 726 | onMessage, | | |
| 727 | }; | | |
| 728 | const initialize = Effect.fn("PreviewManager.initializeControlSession")(function* () { | | |
| 729 | yield* Ref.update(diagnosticsRef, (diagnostics) => | | |
| 730 | replaceMap(diagnostics, (copy) => { | | |
| 731 | copy.set(wc.id, { | | |
| 732 | consoleEntries: [], | | |
| 733 | networkEntries: [], | | |
| 734 | requests: new Map(), | | |
| 735 | }); | | |
| 736 | }), | 697 | }), |
| 737 | ); | 698 | ); |
| 738 | yield* attempt("attachDebuggerListeners", () => { | 699 | } |
| 739 | wc.debugger.on("message", onMessage); | 700 | if (wc.debugger.isAttached()) { |
| 740 | wc.debugger.attach("1.3"); | 701 | return Effect.fail( |
| 741 | }); | 702 | new PreviewAutomationDebuggerAttachedError({ |
| 742 | yield* Effect.all( | 703 | webContentsId: wc.id, |
| 743 | ["Runtime.enable", "Accessibility.enable", "Network.enable", "Log.enable"].map( | 704 | }), |
| 744 | (method) => | 705 | ); |
| 745 | attemptPromise("initializeDebugger", () => wc.debugger.sendCommand(method)), | 706 | } |
| | 707 | const createControlSession = Effect.fn("PreviewManager.createControlSession")(function* () { |
| | 708 | const semaphore = yield* Semaphore.make(1); |
| | 709 | const scope = yield* Scope.fork(parentScope, "sequential"); |
| | 710 | const handleDebuggerMessage = Effect.fn("PreviewManager.handleDebuggerMessage")( |
| | 711 | function* (method: string, params: Record<string, unknown>) { |
| | 712 | if (method === "Page.screencastFrame") { |
| | 713 | const sessionId = params["sessionId"]; |
| | 714 | if (typeof sessionId === "number") { |
| | 715 | yield* attemptPromise( |
| | 716 | { |
| | 717 | operation: "ackScreencastFrame", |
| | 718 | webContentsId: wc.id, |
| | 719 | }, |
| | 720 | () => wc.debugger.sendCommand("Page.screencastFrameAck", { sessionId }), |
| | 721 | ).pipe(Effect.ignore); |
| | 722 | } |
| | 723 | const tabId = yield* tabIdForWebContents(wc.id); |
| | 724 | const metadata = |
| | 725 | typeof params["metadata"] === "object" && params["metadata"] !== null |
| | 726 | ? (params["metadata"] as Record<string, unknown>) |
| | 727 | : {}; |
| | 728 | if (tabId && typeof params["data"] === "string") { |
| | 729 | const receivedAt = yield* currentIso; |
| | 730 | const listeners = yield* Ref.get(recordingFrameListenersRef); |
| | 731 | const frame: DesktopPreviewRecordingFrame = { |
| | 732 | tabId, |
| | 733 | data: params["data"], |
| | 734 | width: |
| | 735 | typeof metadata["deviceWidth"] === "number" ? metadata["deviceWidth"] : 0, |
| | 736 | height: |
| | 737 | typeof metadata["deviceHeight"] === "number" ? metadata["deviceHeight"] : 0, |
| | 738 | receivedAt, |
| | 739 | }; |
| | 740 | yield* Effect.forEach( |
| | 741 | listeners, |
| | 742 | (listener) => Effect.sync(() => listener(frame)).pipe(Effect.ignore), |
| | 743 | { discard: true }, |
| | 744 | ); |
| | 745 | } |
| | 746 | } |
| | 747 | yield* captureDiagnosticMessage(wc.id, method, params); |
| | 748 | }, |
| | 749 | ); |
| | 750 | const onMessage: BrowserControlSession["onMessage"] = (_event, method, params) => { |
| | 751 | runFork(handleDebuggerMessage(method, params)); |
| | 752 | }; |
| | 753 | yield* Scope.addFinalizer( |
| | 754 | scope, |
| | 755 | Effect.all( |
| | 756 | [ |
| | 757 | Ref.update(diagnosticsRef, (diagnostics) => |
| | 758 | replaceMap(diagnostics, (copy) => { |
| | 759 | copy.delete(wc.id); |
| | 760 | }), |
| | 761 | ), |
| | 762 | attempt({ operation: "detachControlSession", webContentsId: wc.id }, () => { |
| | 763 | wc.debugger.off("message", onMessage); |
| | 764 | if (wc.debugger.isAttached()) wc.debugger.detach(); |
| | 765 | }).pipe(Effect.ignore), |
| | 766 | ], |
| | 767 | { discard: true }, |
| 746 | ), | 768 | ), |
| 747 | { concurrency: "unbounded", discard: true }, | | |
| 748 | ); | 769 | ); |
| 749 | return [ | 770 | const control: BrowserControlSession = { |
| 750 | control, | 771 | webContentsId: wc.id, |
| 751 | replaceMap(sessions, (copy) => { | 772 | semaphore, |
| 752 | copy.set(wc.id, control); | 773 | scope, |
| 753 | }), | 774 | onMessage, |
| 754 | ] as const; | 775 | }; |
| | 776 | const initialize = Effect.fn("PreviewManager.initializeControlSession")(function* () { |
| | 777 | yield* Ref.update(diagnosticsRef, (diagnostics) => |
| | 778 | replaceMap(diagnostics, (copy) => { |
| | 779 | copy.set(wc.id, { |
| | 780 | consoleEntries: [], |
| | 781 | networkEntries: [], |
| | 782 | requests: new Map(), |
| | 783 | }); |
| | 784 | }), |
| | 785 | ); |
| | 786 | yield* attempt({ operation: "attachDebuggerListeners", webContentsId: wc.id }, () => { |
| | 787 | wc.debugger.on("message", onMessage); |
| | 788 | wc.debugger.attach("1.3"); |
| | 789 | }); |
| | 790 | yield* Effect.all( |
| | 791 | ["Runtime.enable", "Accessibility.enable", "Network.enable", "Log.enable"].map( |
| | 792 | (method) => |
| | 793 | attemptPromise( |
| | 794 | { operation: `initializeDebugger.${method}`, webContentsId: wc.id }, |
| | 795 | () => wc.debugger.sendCommand(method), |
| | 796 | ), |
| | 797 | ), |
| | 798 | { concurrency: "unbounded", discard: true }, |
| | 799 | ); |
| | 800 | return [ |
| | 801 | control, |
| | 802 | replaceMap(sessions, (copy) => { |
| | 803 | copy.set(wc.id, control); |
| | 804 | }), |
| | 805 | ] as const; |
| | 806 | }); |
| | 807 | return yield* initialize().pipe( |
| | 808 | Effect.onError(() => Scope.close(scope, Exit.void).pipe(Effect.ignore)), |
| | 809 | ); |
| 755 | }); | 810 | }); |
| 756 | return yield* initialize().pipe( | 811 | return createControlSession(); |
| 757 | Effect.onError(() => Scope.close(scope, Exit.void).pipe(Effect.ignore)), | 812 | }, |
| 758 | ); | 813 | ); |
| 759 | }); | | |
| 760 | return createControlSession(); | | |
| 761 | }); | | |
| 762 | }); | 814 | }); |
| 763 | | 815 | |
| 764 | const pushAction = (tabId: string, event: PreviewAutomationActionEvent) => | 816 | const pushAction = (tabId: string, event: PreviewAutomationActionEvent) => |
| ... | @@ -808,26 +860,23 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -808,26 +860,23 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 808 | function* (method, commandParams) { | 860 | function* (method, commandParams) { |
| 809 | const before = (yield* Ref.get(controlEpochRef)).get(tabId) ?? 0; | 861 | const before = (yield* Ref.get(controlEpochRef)).get(tabId) ?? 0; |
| 810 | if (before !== epoch) { | 862 | if (before !== epoch) { |
| 811 | return yield* fail( | 863 | return yield* new PreviewAutomationControlInterruptedError({ |
| 812 | action, | 864 | operation: action, |
| 813 | automationError( | 865 | tabId, |
| 814 | "PreviewAutomationControlInterruptedError", | 866 | webContentsId: wc.id, |
| 815 | "Browser control was interrupted by human input.", | 867 | }); |
| 816 | ), | | |
| 817 | ); | | |
| 818 | } | 868 | } |
| 819 | const result = yield* attemptPromise(action, () => | 869 | const result = yield* attemptPromise( |
| 820 | wc.debugger.sendCommand(method, commandParams), | 870 | { operation: `${action}.${method}`, tabId, webContentsId: wc.id }, |
| | 871 | () => wc.debugger.sendCommand(method, commandParams), |
| 821 | ); | 872 | ); |
| 822 | const after = (yield* Ref.get(controlEpochRef)).get(tabId) ?? 0; | 873 | const after = (yield* Ref.get(controlEpochRef)).get(tabId) ?? 0; |
| 823 | if (after !== epoch) { | 874 | if (after !== epoch) { |
| 824 | return yield* fail( | 875 | return yield* new PreviewAutomationControlInterruptedError({ |
| 825 | action, | 876 | operation: action, |
| 826 | automationError( | 877 | tabId, |
| 827 | "PreviewAutomationControlInterruptedError", | 878 | webContentsId: wc.id, |
| 828 | "Browser control was interrupted by human input.", | 879 | }); |
| 829 | ), | | |
| 830 | ); | | |
| 831 | } | 880 | } |
| 832 | return result; | 881 | return result; |
| 833 | }, | 882 | }, |
| ... | @@ -846,15 +895,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -846,15 +895,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 846 | }); | 895 | }); |
| 847 | } else { | 896 | } else { |
| 848 | const error = Option.getOrNull(Cause.findErrorOption(exit.cause)); | 897 | const error = Option.getOrNull(Cause.findErrorOption(exit.cause)); |
| 849 | const underlying = isPreviewManagerError(error) ? error.cause : error; | 898 | const interrupted = isPreviewAutomationControlInterruptedError(error); |
| 850 | const interrupted = | 899 | const errorMessage = isPreviewOperationError(error) |
| 851 | underlying instanceof Error && | 900 | ? PreviewOperationError.toTimelineMessage(error) |
| 852 | underlying.name === "PreviewAutomationControlInterruptedError"; | 901 | : isPreviewAutomationEvaluationError(error) |
| | 902 | ? PreviewAutomationEvaluationError.toTimelineMessage(error) |
| | 903 | : isPreviewAutomationInvalidSelectorError(error) |
| | 904 | ? PreviewAutomationInvalidSelectorError.toTimelineMessage(error) |
| | 905 | : error instanceof Error |
| | 906 | ? error.message |
| | 907 | : String(error); |
| 853 | yield* replaceAction(tabId, { | 908 | yield* replaceAction(tabId, { |
| 854 | ...actionEvent, | 909 | ...actionEvent, |
| 855 | status: interrupted ? "interrupted" : "failed", | 910 | status: interrupted ? "interrupted" : "failed", |
| 856 | completedAt, | 911 | completedAt, |
| 857 | error: underlying instanceof Error ? underlying.message : String(underlying), | 912 | error: errorMessage, |
| 858 | }); | 913 | }); |
| 859 | } | 914 | } |
| 860 | const tabs = yield* SynchronizedRef.get(tabsRef); | 915 | const tabs = yield* SynchronizedRef.get(tabsRef); |
| ... | @@ -864,6 +919,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -864,6 +919,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 864 | }); | 919 | }); |
| 865 | | 920 | |
| 866 | const evaluateWithDebugger = <A = unknown>( | 921 | const evaluateWithDebugger = <A = unknown>( |
| | 922 | tabId: string, |
| 867 | send: SendCommand, | 923 | send: SendCommand, |
| 868 | expression: string, | 924 | expression: string, |
| 869 | returnByValue: boolean, | 925 | returnByValue: boolean, |
| ... | @@ -877,19 +933,18 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -877,19 +933,18 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 877 | }).pipe( | 933 | }).pipe( |
| 878 | Effect.flatMap((rawResponse) => { | 934 | Effect.flatMap((rawResponse) => { |
| 879 | const response = rawResponse as CdpEvaluationResult; | 935 | const response = rawResponse as CdpEvaluationResult; |
| 880 | return response.exceptionDetails | 936 | if (!response.exceptionDetails) { |
| 881 | ? Effect.fail( | 937 | return Effect.succeed(response.result?.value as A); |
| 882 | fail( | 938 | } |
| 883 | "evaluate", | 939 | const detail = previewAutomationEvaluationDetail(response.exceptionDetails); |
| 884 | automationError( | 940 | return Effect.fail( |
| 885 | "PreviewAutomationExecutionError", | 941 | new PreviewAutomationEvaluationError({ |
| 886 | response.exceptionDetails.exception?.description ?? | 942 | tabId, |
| 887 | response.exceptionDetails.text ?? | 943 | detailKind: detail.detailKind, |
| 888 | "JavaScript evaluation failed.", | 944 | detailLength: detail.detail?.length ?? 0, |
| 889 | ), | 945 | cause: response.exceptionDetails, |
| 890 | ), | 946 | }), |
| 891 | ) | 947 | ); |
| 892 | : Effect.succeed(response.result?.value as A); | | |
| 893 | }), | 948 | }), |
| 894 | ); | 949 | ); |
| 895 | | 950 | |
| ... | @@ -898,17 +953,44 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -898,17 +953,44 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 898 | readonly locator?: string | undefined; | 953 | readonly locator?: string | undefined; |
| 899 | }): string | null => input.locator ?? (input.selector ? `css=${input.selector}` : null); | 954 | }): string | null => input.locator ?? (input.selector ? `css=${input.selector}` : null); |
| 900 | | 955 | |
| | 956 | const automationSelectorDiagnostics = (input: { |
| | 957 | readonly selector?: string | undefined; |
| | 958 | readonly locator?: string | undefined; |
| | 959 | }): { |
| | 960 | readonly selectorKind: PreviewAutomationSelectorKind; |
| | 961 | readonly selectorLength?: number; |
| | 962 | } => { |
| | 963 | if (input.locator !== undefined) { |
| | 964 | return { selectorKind: "locator", selectorLength: input.locator.length }; |
| | 965 | } |
| | 966 | if (input.selector !== undefined) { |
| | 967 | return { selectorKind: "selector", selectorLength: input.selector.length }; |
| | 968 | } |
| | 969 | return { selectorKind: "focused-element" }; |
| | 970 | }; |
| | 971 | |
| 901 | const ensurePlaywrightInjected = Effect.fn("PreviewManager.ensurePlaywrightInjected")(function* ( | 972 | const ensurePlaywrightInjected = Effect.fn("PreviewManager.ensurePlaywrightInjected")(function* ( |
| | 973 | tabId: string, |
| 902 | send: SendCommand, | 974 | send: SendCommand, |
| 903 | ) { | 975 | ) { |
| 904 | const installed = yield* evaluateWithDebugger<boolean>( | 976 | const installed = yield* evaluateWithDebugger<boolean>( |
| | 977 | tabId, |
| 905 | send, | 978 | send, |
| 906 | "Boolean(globalThis.__t3PlaywrightInjected)", | 979 | "Boolean(globalThis.__t3PlaywrightInjected)", |
| 907 | true, | 980 | true, |
| 908 | ); | 981 | ); |
| 909 | if (installed) return; | 982 | if (installed) return; |
| 910 | const expression = yield* playwrightInstallExpression; | 983 | const expression = yield* playwrightInstallExpression.pipe( |
| 911 | yield* evaluateWithDebugger(send, expression, true); | 984 | Effect.mapError( |
| | 985 | (cause) => |
| | 986 | new PreviewOperationError({ |
| | 987 | operation: "ensurePlaywrightInjected", |
| | 988 | tabId, |
| | 989 | cause, |
| | 990 | }), |
| | 991 | ), |
| | 992 | ); |
| | 993 | yield* evaluateWithDebugger(tabId, send, expression, true); |
| 912 | }); | 994 | }); |
| 913 | | 995 | |
| 914 | const cancelPickElement = Effect.fn("PreviewManager.cancelPickElement")(function* ( | 996 | const cancelPickElement = Effect.fn("PreviewManager.cancelPickElement")(function* ( |
| ... | @@ -1060,7 +1142,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1060,7 +1142,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1060 | }; | 1142 | }; |
| 1061 | yield* Scope.addFinalizer( | 1143 | yield* Scope.addFinalizer( |
| 1062 | scope, | 1144 | scope, |
| 1063 | attempt("detachListeners", () => { | 1145 | attempt({ operation: "detachListeners", tabId, webContentsId: wc.id }, () => { |
| 1064 | wc.off("did-navigate", sync); | 1146 | wc.off("did-navigate", sync); |
| 1065 | wc.off("did-navigate-in-page", sync); | 1147 | wc.off("did-navigate-in-page", sync); |
| 1066 | wc.off("page-title-updated", sync); | 1148 | wc.off("page-title-updated", sync); |
| ... | @@ -1072,7 +1154,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1072,7 +1154,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1072 | }).pipe(Effect.ignore), | 1154 | }).pipe(Effect.ignore), |
| 1073 | ); | 1155 | ); |
| 1074 | const install = Effect.fn("PreviewManager.installWebContentsListeners")(function* () { | 1156 | const install = Effect.fn("PreviewManager.installWebContentsListeners")(function* () { |
| 1075 | yield* attempt("attachListeners", () => { | 1157 | yield* attempt({ operation: "attachListeners", tabId, webContentsId: wc.id }, () => { |
| 1076 | wc.on("did-navigate", sync); | 1158 | wc.on("did-navigate", sync); |
| 1077 | wc.on("did-navigate-in-page", sync); | 1159 | wc.on("did-navigate-in-page", sync); |
| 1078 | wc.on("page-title-updated", sync); | 1160 | wc.on("page-title-updated", sync); |
| ... | @@ -1081,7 +1163,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1081,7 +1163,11 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1081 | wc.on("did-fail-load", failed as never); | 1163 | wc.on("did-fail-load", failed as never); |
| 1082 | wc.ipc.on(HUMAN_INPUT_CHANNEL, humanInput); | 1164 | wc.ipc.on(HUMAN_INPUT_CHANNEL, humanInput); |
| 1083 | wc.setWindowOpenHandler(({ url }) => { | 1165 | wc.setWindowOpenHandler(({ url }) => { |
| 1084 | runFork(attemptPromise("openPreviewWindow", () => wc.loadURL(url)).pipe(Effect.ignore)); | 1166 | runFork( |
| | 1167 | attemptPromise({ operation: "openPreviewWindow", tabId, webContentsId: wc.id }, () => |
| | 1168 | wc.loadURL(url), |
| | 1169 | ).pipe(Effect.ignore), |
| | 1170 | ); |
| 1085 | return { action: "deny" }; | 1171 | return { action: "deny" }; |
| 1086 | }); | 1172 | }); |
| 1087 | wc.on("before-input-event", beforeInput); | 1173 | wc.on("before-input-event", beforeInput); |
| ... | @@ -1162,7 +1248,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1162,7 +1248,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1162 | ) { | 1248 | ) { |
| 1163 | const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId); | 1249 | const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId); |
| 1164 | if (!tab) { | 1250 | if (!tab) { |
| 1165 | return yield* fail("registerWebview", new PreviewTabNotFoundError({ tabId })); | 1251 | return yield* new PreviewTabNotFoundError({ tabId }); |
| 1166 | } | 1252 | } |
| 1167 | const wc = webContents.fromId(webContentsId); | 1253 | const wc = webContents.fromId(webContentsId); |
| 1168 | const mainWindow = yield* Ref.get(mainWindowRef); | 1254 | const mainWindow = yield* Ref.get(mainWindowRef); |
| ... | @@ -1171,15 +1257,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1171,15 +1257,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1171 | wc.getType() !== "webview" || | 1257 | wc.getType() !== "webview" || |
| 1172 | (Option.isSome(mainWindow) && wc.hostWebContents !== mainWindow.value.webContents) | 1258 | (Option.isSome(mainWindow) && wc.hostWebContents !== mainWindow.value.webContents) |
| 1173 | ) { | 1259 | ) { |
| 1174 | return yield* fail( | 1260 | return yield* new PreviewWebContentsNotFoundError({ tabId, webContentsId }); |
| 1175 | "registerWebview", | | |
| 1176 | new PreviewWebContentsNotFoundError({ tabId, webContentsId }), | | |
| 1177 | ); | | |
| 1178 | } | 1261 | } |
| 1179 | const attached = yield* Ref.get(attachedRef); | 1262 | const attached = yield* Ref.get(attachedRef); |
| 1180 | const annotationTheme = yield* Ref.get(annotationThemeRef); | 1263 | const annotationTheme = yield* Ref.get(annotationThemeRef); |
| 1181 | if (tab.webContentsId === webContentsId && attached.has(webContentsId)) { | 1264 | if (tab.webContentsId === webContentsId && attached.has(webContentsId)) { |
| 1182 | yield* attempt("registerWebview.sendTheme", () => | 1265 | yield* attempt({ operation: "registerWebview.sendTheme", tabId, webContentsId }, () => |
| 1183 | wc.send(ANNOTATION_THEME_CHANNEL, annotationTheme), | 1266 | wc.send(ANNOTATION_THEME_CHANNEL, annotationTheme), |
| 1184 | ); | 1267 | ); |
| 1185 | return; | 1268 | return; |
| ... | @@ -1225,16 +1308,16 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1225,16 +1308,16 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1225 | ] as const; | 1308 | ] as const; |
| 1226 | }); | 1309 | }); |
| 1227 | if (Option.isNone(registration)) { | 1310 | if (Option.isNone(registration)) { |
| 1228 | return yield* fail("registerWebview", new PreviewTabNotFoundError({ tabId })); | 1311 | return yield* new PreviewTabNotFoundError({ tabId }); |
| 1229 | } | 1312 | } |
| 1230 | const { state: registered, pendingUrl } = registration.value; | 1313 | const { state: registered, pendingUrl } = registration.value; |
| 1231 | yield* emit(tabId, registered); | 1314 | yield* emit(tabId, registered); |
| 1232 | if (Math.abs(registered.zoomFactor - DEFAULT_ZOOM_FACTOR) > ZOOM_EPSILON) { | 1315 | if (Math.abs(registered.zoomFactor - DEFAULT_ZOOM_FACTOR) > ZOOM_EPSILON) { |
| 1233 | yield* attempt("registerWebview.restoreZoom", () => | 1316 | yield* attempt({ operation: "registerWebview.restoreZoom", tabId, webContentsId }, () => |
| 1234 | wc.setZoomFactor(registered.zoomFactor), | 1317 | wc.setZoomFactor(registered.zoomFactor), |
| 1235 | ).pipe(Effect.ignore); | 1318 | ).pipe(Effect.ignore); |
| 1236 | } | 1319 | } |
| 1237 | yield* attempt("registerWebview.sendTheme", () => | 1320 | yield* attempt({ operation: "registerWebview.sendTheme", tabId, webContentsId }, () => |
| 1238 | wc.send(ANNOTATION_THEME_CHANNEL, annotationTheme), | 1321 | wc.send(ANNOTATION_THEME_CHANNEL, annotationTheme), |
| 1239 | ); | 1322 | ); |
| 1240 | const latestNavStatus = (yield* SynchronizedRef.get(tabsRef)).get(tabId)?.navStatus; | 1323 | const latestNavStatus = (yield* SynchronizedRef.get(tabsRef)).get(tabId)?.navStatus; |
| ... | @@ -1245,15 +1328,17 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1245,15 +1328,17 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1245 | wc.getURL() !== pendingUrl | 1328 | wc.getURL() !== pendingUrl |
| 1246 | ) { | 1329 | ) { |
| 1247 | runFork( | 1330 | runFork( |
| 1248 | attemptPromise("registerWebview.loadPendingUrl", () => wc.loadURL(pendingUrl)).pipe( | 1331 | attemptPromise({ operation: "registerWebview.loadPendingUrl", tabId, webContentsId }, () => |
| 1249 | Effect.ignore, | 1332 | wc.loadURL(pendingUrl), |
| 1250 | ), | 1333 | ).pipe(Effect.ignore), |
| 1251 | ); | 1334 | ); |
| 1252 | } | 1335 | } |
| 1253 | }); | 1336 | }); |
| 1254 | | 1337 | |
| 1255 | const navigate = Effect.fn("PreviewManager.navigate")(function* (tabId: string, rawUrl: string) { | 1338 | const navigate = Effect.fn("PreviewManager.navigate")(function* (tabId: string, rawUrl: string) { |
| 1256 | const url = yield* attempt("navigate.normalizeUrl", () => normalizePreviewUrl(rawUrl)); | 1339 | const url = yield* attempt({ operation: "navigate.normalizeUrl", tabId }, () => |
| | 1340 | normalizePreviewUrl(rawUrl), |
| | 1341 | ); |
| 1257 | const updatedAt = yield* currentIso; | 1342 | const updatedAt = yield* currentIso; |
| 1258 | const pending = yield* SynchronizedRef.modify(tabsRef, (tabs) => { | 1343 | const pending = yield* SynchronizedRef.modify(tabsRef, (tabs) => { |
| 1259 | const current = tabs.get(tabId); | 1344 | const current = tabs.get(tabId); |
| ... | @@ -1294,10 +1379,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1294,10 +1379,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1294 | return; | 1379 | return; |
| 1295 | } | 1380 | } |
| 1296 | if (wc.getURL() === url) { | 1381 | if (wc.getURL() === url) { |
| 1297 | yield* attempt("navigate.reload", () => wc.reload()); | 1382 | yield* attempt({ operation: "navigate.reload", tabId, webContentsId: wc.id }, () => |
| | 1383 | wc.reload(), |
| | 1384 | ); |
| 1298 | return; | 1385 | return; |
| 1299 | } | 1386 | } |
| 1300 | yield* attemptPromise("navigate.loadURL", () => wc.loadURL(url)); | 1387 | yield* attemptPromise({ operation: "navigate.loadURL", tabId, webContentsId: wc.id }, () => |
| | 1388 | wc.loadURL(url), |
| | 1389 | ); |
| 1301 | }); | 1390 | }); |
| 1302 | | 1391 | |
| 1303 | const withWebContents = Effect.fn("PreviewManager.withWebContents")(function* ( | 1392 | const withWebContents = Effect.fn("PreviewManager.withWebContents")(function* ( |
| ... | @@ -1306,7 +1395,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1306,7 +1395,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1306 | use: (wc: Electron.WebContents) => void, | 1395 | use: (wc: Electron.WebContents) => void, |
| 1307 | ) { | 1396 | ) { |
| 1308 | const wc = yield* requireWebContents(tabId); | 1397 | const wc = yield* requireWebContents(tabId); |
| 1309 | yield* attempt(operation, () => use(wc)); | 1398 | yield* attempt({ operation, tabId, webContentsId: wc.id }, () => use(wc)); |
| 1310 | }); | 1399 | }); |
| 1311 | | 1400 | |
| 1312 | const goBack = (tabId: string) => | 1401 | const goBack = (tabId: string) => |
| ... | @@ -1324,11 +1413,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1324,11 +1413,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1324 | const openDevTools = Effect.fn("PreviewManager.openDevTools")(function* (tabId: string) { | 1413 | const openDevTools = Effect.fn("PreviewManager.openDevTools")(function* (tabId: string) { |
| 1325 | const wc = yield* requireWebContents(tabId); | 1414 | const wc = yield* requireWebContents(tabId); |
| 1326 | if (wc.isDevToolsOpened()) { | 1415 | if (wc.isDevToolsOpened()) { |
| 1327 | yield* attempt("openDevTools.focus", () => wc.devToolsWebContents?.focus()); | 1416 | yield* attempt({ operation: "openDevTools.focus", tabId, webContentsId: wc.id }, () => |
| | 1417 | wc.devToolsWebContents?.focus(), |
| | 1418 | ); |
| 1328 | return; | 1419 | return; |
| 1329 | } | 1420 | } |
| 1330 | yield* detachControlSession(wc.id); | 1421 | yield* detachControlSession(wc.id); |
| 1331 | yield* attempt("openDevTools", () => { | 1422 | yield* attempt({ operation: "openDevTools", tabId, webContentsId: wc.id }, () => { |
| 1332 | wc.once("devtools-closed", () => { | 1423 | wc.once("devtools-closed", () => { |
| 1333 | if (!wc.isDestroyed()) runFork(ensureControlSession(wc).pipe(Effect.ignore)); | 1424 | if (!wc.isDestroyed()) runFork(ensureControlSession(wc).pipe(Effect.ignore)); |
| 1334 | }); | 1425 | }); |
| ... | @@ -1348,9 +1439,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1348,9 +1439,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1348 | const wc = webContents.fromId(tab.webContentsId); | 1439 | const wc = webContents.fromId(tab.webContentsId); |
| 1349 | return !wc || wc.isDestroyed() | 1440 | return !wc || wc.isDestroyed() |
| 1350 | ? Effect.void | 1441 | ? Effect.void |
| 1351 | : attempt("setAnnotationTheme", () => wc.send(ANNOTATION_THEME_CHANNEL, theme)).pipe( | 1442 | : attempt( |
| 1352 | Effect.ignore, | 1443 | { |
| 1353 | ); | 1444 | operation: "setAnnotationTheme", |
| | 1445 | tabId: tab.tabId, |
| | 1446 | webContentsId: tab.webContentsId, |
| | 1447 | }, |
| | 1448 | () => wc.send(ANNOTATION_THEME_CHANNEL, theme), |
| | 1449 | ).pipe(Effect.ignore); |
| 1354 | }, | 1450 | }, |
| 1355 | { discard: true }, | 1451 | { discard: true }, |
| 1356 | ); | 1452 | ); |
| ... | @@ -1363,7 +1459,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1363,7 +1459,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1363 | return yield* Effect.callback<PreviewAnnotationPayload | null, PreviewManagerError>( | 1459 | return yield* Effect.callback<PreviewAnnotationPayload | null, PreviewManagerError>( |
| 1364 | (resume) => { | 1460 | (resume) => { |
| 1365 | const cleanup = Effect.fn("PreviewManager.cleanupPickElement")(function* () { | 1461 | const cleanup = Effect.fn("PreviewManager.cleanupPickElement")(function* () { |
| 1366 | yield* attempt("pickElement.cleanup", () => { | 1462 | yield* attempt({ operation: "pickElement.cleanup", tabId, webContentsId: wc.id }, () => { |
| 1367 | wc.ipc.removeListener(ELEMENT_PICKED_CHANNEL, onMessage); | 1463 | wc.ipc.removeListener(ELEMENT_PICKED_CHANNEL, onMessage); |
| 1368 | wc.off("destroyed", onDestroyed); | 1464 | wc.off("destroyed", onDestroyed); |
| 1369 | wc.off("did-start-navigation", onNavigated); | 1465 | wc.off("did-start-navigation", onNavigated); |
| ... | @@ -1392,9 +1488,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1392,9 +1488,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1392 | if (activeTab?.webContentsId != null) { | 1488 | if (activeTab?.webContentsId != null) { |
| 1393 | const activeWc = webContents.fromId(activeTab.webContentsId); | 1489 | const activeWc = webContents.fromId(activeTab.webContentsId); |
| 1394 | if (activeWc && !activeWc.isDestroyed()) { | 1490 | if (activeWc && !activeWc.isDestroyed()) { |
| 1395 | yield* attempt("cancelPickElement", () => activeWc.send(CANCEL_PICK_CHANNEL)).pipe( | 1491 | yield* attempt( |
| 1396 | Effect.ignore, | 1492 | { |
| 1397 | ); | 1493 | operation: "cancelPickElement", |
| | 1494 | tabId, |
| | 1495 | webContentsId: activeWc.id, |
| | 1496 | }, |
| | 1497 | () => activeWc.send(CANCEL_PICK_CHANNEL), |
| | 1498 | ).pipe(Effect.ignore); |
| 1398 | } | 1499 | } |
| 1399 | } | 1500 | } |
| 1400 | resume(Effect.succeed(null)); | 1501 | resume(Effect.succeed(null)); |
| ... | @@ -1408,15 +1509,18 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1408,15 +1509,18 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1408 | } | 1509 | } |
| 1409 | const cropRect = normalizeCaptureRect(args[1]); | 1510 | const cropRect = normalizeCaptureRect(args[1]); |
| 1410 | runFork( | 1511 | runFork( |
| 1411 | captureAnnotationScreenshot(wc, cropRect).pipe( | 1512 | captureAnnotationScreenshot(tabId, wc, cropRect).pipe( |
| 1412 | Effect.matchEffect({ | 1513 | Effect.matchEffect({ |
| 1413 | onFailure: () => Effect.sync(() => settle(payload)), | 1514 | onFailure: () => Effect.sync(() => settle(payload)), |
| 1414 | onSuccess: (screenshot) => Effect.sync(() => settle({ ...payload, screenshot })), | 1515 | onSuccess: (screenshot) => Effect.sync(() => settle({ ...payload, screenshot })), |
| 1415 | }), | 1516 | }), |
| 1416 | Effect.ensuring( | 1517 | Effect.ensuring( |
| 1417 | attempt("pickElement.captureComplete", () => { | 1518 | attempt( |
| 1418 | if (!wc.isDestroyed()) wc.send(ANNOTATION_CAPTURED_CHANNEL); | 1519 | { operation: "pickElement.captureComplete", tabId, webContentsId: wc.id }, |
| 1419 | }).pipe(Effect.ignore), | 1520 | () => { |
| | 1521 | if (!wc.isDestroyed()) wc.send(ANNOTATION_CAPTURED_CHANNEL); |
| | 1522 | }, |
| | 1523 | ).pipe(Effect.ignore), |
| 1420 | ), | 1524 | ), |
| 1421 | ), | 1525 | ), |
| 1422 | ); | 1526 | ); |
| ... | @@ -1431,7 +1535,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1431,7 +1535,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1431 | if (isMainFrame) settle(null); | 1535 | if (isMainFrame) settle(null); |
| 1432 | }; | 1536 | }; |
| 1433 | const registerPickElement = Effect.fn("PreviewManager.registerPickElement")(function* () { | 1537 | const registerPickElement = Effect.fn("PreviewManager.registerPickElement")(function* () { |
| 1434 | yield* attempt("pickElement.register", () => { | 1538 | yield* attempt({ operation: "pickElement.register", tabId, webContentsId: wc.id }, () => { |
| 1435 | wc.ipc.on(ELEMENT_PICKED_CHANNEL, onMessage); | 1539 | wc.ipc.on(ELEMENT_PICKED_CHANNEL, onMessage); |
| 1436 | wc.once("destroyed", onDestroyed); | 1540 | wc.once("destroyed", onDestroyed); |
| 1437 | wc.once("did-start-navigation", onNavigated); | 1541 | wc.once("did-start-navigation", onNavigated); |
| ... | @@ -1468,7 +1572,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1468,7 +1572,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1468 | if (tab.webContentsId != null) { | 1572 | if (tab.webContentsId != null) { |
| 1469 | const wc = webContents.fromId(tab.webContentsId); | 1573 | const wc = webContents.fromId(tab.webContentsId); |
| 1470 | if (wc && !wc.isDestroyed()) { | 1574 | if (wc && !wc.isDestroyed()) { |
| 1471 | yield* attempt("applyZoom", () => wc.setZoomFactor(next)); | 1575 | yield* attempt({ operation: "applyZoom", tabId, webContentsId: wc.id }, () => |
| | 1576 | wc.setZoomFactor(next), |
| | 1577 | ); |
| 1472 | } | 1578 | } |
| 1473 | } | 1579 | } |
| 1474 | yield* update(tabId, { zoomFactor: next }); | 1580 | yield* update(tabId, { zoomFactor: next }); |
| ... | @@ -1481,17 +1587,42 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1481,17 +1587,42 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1481 | const [createdAt, millis, image] = yield* Effect.all([ | 1587 | const [createdAt, millis, image] = yield* Effect.all([ |
| 1482 | currentIso, | 1588 | currentIso, |
| 1483 | currentMillis, | 1589 | currentMillis, |
| 1484 | attemptPromise("captureScreenshot.capturePage", () => wc.capturePage()), | 1590 | attemptPromise( |
| | 1591 | { |
| | 1592 | operation: "captureScreenshot.capturePage", |
| | 1593 | tabId, |
| | 1594 | webContentsId: wc.id, |
| | 1595 | }, |
| | 1596 | () => wc.capturePage(), |
| | 1597 | ), |
| 1485 | ]); | 1598 | ]); |
| 1486 | const id = `browser-screenshot-${artifactSiteSlug(wc.getURL())}-${millis.toString(36)}`; | 1599 | const id = `browser-screenshot-${artifactSiteSlug(wc.getURL())}-${millis.toString(36)}`; |
| 1487 | const artifactPath = path.join(resolvedArtifactDirectory, `${id}.png`); | 1600 | const artifactPath = path.join(resolvedArtifactDirectory, `${id}.png`); |
| 1488 | const data = image.toPNG(); | 1601 | const data = image.toPNG(); |
| 1489 | yield* fileSystem | 1602 | yield* fileSystem.makeDirectory(resolvedArtifactDirectory, { recursive: true }).pipe( |
| 1490 | .makeDirectory(resolvedArtifactDirectory, { recursive: true }) | 1603 | Effect.mapError( |
| 1491 | .pipe(Effect.mapError((cause) => fail("captureScreenshot.makeDirectory", cause))); | 1604 | (cause) => |
| 1492 | yield* fileSystem | 1605 | new PreviewOperationError({ |
| 1493 | .writeFile(artifactPath, data) | 1606 | operation: "captureScreenshot.makeDirectory", |
| 1494 | .pipe(Effect.mapError((cause) => fail("captureScreenshot.writeFile", cause))); | 1607 | tabId, |
| | 1608 | webContentsId: wc.id, |
| | 1609 | artifactPath, |
| | 1610 | cause, |
| | 1611 | }), |
| | 1612 | ), |
| | 1613 | ); |
| | 1614 | yield* fileSystem.writeFile(artifactPath, data).pipe( |
| | 1615 | Effect.mapError( |
| | 1616 | (cause) => |
| | 1617 | new PreviewOperationError({ |
| | 1618 | operation: "captureScreenshot.writeFile", |
| | 1619 | tabId, |
| | 1620 | webContentsId: wc.id, |
| | 1621 | artifactPath, |
| | 1622 | cause, |
| | 1623 | }), |
| | 1624 | ), |
| | 1625 | ); |
| 1495 | return { | 1626 | return { |
| 1496 | id, | 1627 | id, |
| 1497 | tabId, | 1628 | tabId, |
| ... | @@ -1518,10 +1649,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1518,10 +1649,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1518 | const startRecording = Effect.fn("PreviewManager.startRecording")(function* (tabId: string) { | 1649 | const startRecording = Effect.fn("PreviewManager.startRecording")(function* (tabId: string) { |
| 1519 | const recordingTabId = yield* Ref.get(recordingTabIdRef); | 1650 | const recordingTabId = yield* Ref.get(recordingTabIdRef); |
| 1520 | if (Option.isSome(recordingTabId) && recordingTabId.value !== tabId) { | 1651 | if (Option.isSome(recordingTabId) && recordingTabId.value !== tabId) { |
| 1521 | return yield* fail( | 1652 | return yield* new PreviewRecordingAlreadyActiveError({ |
| 1522 | "startRecording", | 1653 | requestedTabId: tabId, |
| 1523 | new Error("Only one browser recording can be active per window."), | 1654 | activeTabId: recordingTabId.value, |
| 1524 | ); | 1655 | }); |
| 1525 | } | 1656 | } |
| 1526 | const wc = yield* requireWebContents(tabId); | 1657 | const wc = yield* requireWebContents(tabId); |
| 1527 | yield* withControlSession(tabId, wc, "recording.start", startScreencast); | 1658 | yield* withControlSession(tabId, wc, "recording.start", startScreencast); |
| ... | @@ -1547,12 +1678,28 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1547,12 +1678,28 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1547 | const id = `browser-recording-${millis.toString(36)}`; | 1678 | const id = `browser-recording-${millis.toString(36)}`; |
| 1548 | const extension = mimeType.includes("mp4") ? "mp4" : "webm"; | 1679 | const extension = mimeType.includes("mp4") ? "mp4" : "webm"; |
| 1549 | const artifactPath = path.join(resolvedArtifactDirectory, `${id}.${extension}`); | 1680 | const artifactPath = path.join(resolvedArtifactDirectory, `${id}.${extension}`); |
| 1550 | yield* fileSystem | 1681 | yield* fileSystem.makeDirectory(resolvedArtifactDirectory, { recursive: true }).pipe( |
| 1551 | .makeDirectory(resolvedArtifactDirectory, { recursive: true }) | 1682 | Effect.mapError( |
| 1552 | .pipe(Effect.mapError((cause) => fail("saveRecording.makeDirectory", cause))); | 1683 | (cause) => |
| 1553 | yield* fileSystem | 1684 | new PreviewOperationError({ |
| 1554 | .writeFile(artifactPath, data) | 1685 | operation: "saveRecording.makeDirectory", |
| 1555 | .pipe(Effect.mapError((cause) => fail("saveRecording.writeFile", cause))); | 1686 | tabId, |
| | 1687 | artifactPath, |
| | 1688 | cause, |
| | 1689 | }), |
| | 1690 | ), |
| | 1691 | ); |
| | 1692 | yield* fileSystem.writeFile(artifactPath, data).pipe( |
| | 1693 | Effect.mapError( |
| | 1694 | (cause) => |
| | 1695 | new PreviewOperationError({ |
| | 1696 | operation: "saveRecording.writeFile", |
| | 1697 | tabId, |
| | 1698 | artifactPath, |
| | 1699 | cause, |
| | 1700 | }), |
| | 1701 | ), |
| | 1702 | ); |
| 1556 | return { | 1703 | return { |
| 1557 | id, | 1704 | id, |
| 1558 | tabId, | 1705 | tabId, |
| ... | @@ -1609,6 +1756,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1609,6 +1756,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1609 | visibleText: string; | 1756 | visibleText: string; |
| 1610 | interactiveElements: PreviewAutomationSnapshot["interactiveElements"]; | 1757 | interactiveElements: PreviewAutomationSnapshot["interactiveElements"]; |
| 1611 | }>( | 1758 | }>( |
| | 1759 | tabId, |
| 1612 | send, | 1760 | send, |
| 1613 | `(() => { | 1761 | `(() => { |
| 1614 | const selectorFor = (element) => { | 1762 | const selectorFor = (element) => { |
| ... | @@ -1665,7 +1813,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1665,7 +1813,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1665 | ); | 1813 | ); |
| 1666 | const [accessibility, sourceImage, diagnostics, timelines] = yield* Effect.all([ | 1814 | const [accessibility, sourceImage, diagnostics, timelines] = yield* Effect.all([ |
| 1667 | send("Accessibility.getFullAXTree"), | 1815 | send("Accessibility.getFullAXTree"), |
| 1668 | attemptPromise("automationSnapshot.capturePage", () => wc.capturePage()), | 1816 | attemptPromise( |
| | 1817 | { |
| | 1818 | operation: "automationSnapshot.capturePage", |
| | 1819 | tabId, |
| | 1820 | webContentsId: wc.id, |
| | 1821 | }, |
| | 1822 | () => wc.capturePage(), |
| | 1823 | ), |
| 1669 | Ref.get(diagnosticsRef), | 1824 | Ref.get(diagnosticsRef), |
| 1670 | Ref.get(actionTimelineRef), | 1825 | Ref.get(actionTimelineRef), |
| 1671 | ]); | 1826 | ]); |
| ... | @@ -1702,6 +1857,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1702,6 +1857,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1702 | }); | 1857 | }); |
| 1703 | | 1858 | |
| 1704 | const resolveClickPoint = Effect.fn("PreviewManager.resolveClickPoint")(function* ( | 1859 | const resolveClickPoint = Effect.fn("PreviewManager.resolveClickPoint")(function* ( |
| | 1860 | tabId: string, |
| 1705 | send: SendCommand, | 1861 | send: SendCommand, |
| 1706 | input: PreviewAutomationClickInput, | 1862 | input: PreviewAutomationClickInput, |
| 1707 | ) { | 1863 | ) { |
| ... | @@ -1709,11 +1865,15 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1709,11 +1865,15 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1709 | return { x: input.x!, y: input.y! }; | 1865 | return { x: input.x!, y: input.y! }; |
| 1710 | } | 1866 | } |
| 1711 | const locator = automationLocator(input)!; | 1867 | const locator = automationLocator(input)!; |
| 1712 | yield* ensurePlaywrightInjected(send); | 1868 | yield* ensurePlaywrightInjected(tabId, send); |
| 1713 | const locatorJson = yield* encodeJson("automationClick.encodeLocator", locator); | 1869 | const locatorJson = yield* encodeJson( |
| | 1870 | { operation: "automationClick.encodeLocator", tabId }, |
| | 1871 | locator, |
| | 1872 | ); |
| 1714 | const point = yield* evaluateWithDebugger< | 1873 | const point = yield* evaluateWithDebugger< |
| 1715 | { x: number; y: number } | { invalidSelector: true; message: string } | { notFound: true } | 1874 | { x: number; y: number } | { invalidSelector: true; message: string } | { notFound: true } |
| 1716 | >( | 1875 | >( |
| | 1876 | tabId, |
| 1717 | send, | 1877 | send, |
| 1718 | `(() => { | 1878 | `(() => { |
| 1719 | try { | 1879 | try { |
| ... | @@ -1734,21 +1894,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1734,21 +1894,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1734 | true, | 1894 | true, |
| 1735 | ); | 1895 | ); |
| 1736 | if ("invalidSelector" in point) { | 1896 | if ("invalidSelector" in point) { |
| 1737 | return yield* fail( | 1897 | return yield* new PreviewAutomationInvalidSelectorError({ |
| 1738 | "automationClick", | 1898 | operation: "click", |
| 1739 | automationError("PreviewAutomationInvalidSelectorError", point.message, { | 1899 | tabId, |
| 1740 | selector: locator, | 1900 | ...automationSelectorDiagnostics(input), |
| 1741 | }), | 1901 | reasonLength: point.message.length, |
| 1742 | ); | 1902 | cause: point, |
| | 1903 | }); |
| 1743 | } | 1904 | } |
| 1744 | if ("notFound" in point) { | 1905 | if ("notFound" in point) { |
| 1745 | return yield* fail( | 1906 | return yield* new PreviewAutomationTargetNotFoundError({ |
| 1746 | "automationClick", | 1907 | operation: "click", |
| 1747 | automationError( | 1908 | tabId, |
| 1748 | "PreviewAutomationExecutionError", | 1909 | ...automationSelectorDiagnostics(input), |
| 1749 | `No element matches locator ${locator}.`, | 1910 | }); |
| 1750 | ), | | |
| 1751 | ); | | |
| 1752 | } | 1911 | } |
| 1753 | return point; | 1912 | return point; |
| 1754 | }); | 1913 | }); |
| ... | @@ -1773,20 +1932,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1773,20 +1932,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1773 | [send("Runtime.enable"), send("Input.setIgnoreInputEvents", { ignore: false })], | 1932 | [send("Runtime.enable"), send("Input.setIgnoreInputEvents", { ignore: false })], |
| 1774 | { concurrency: 2, discard: true }, | 1933 | { concurrency: 2, discard: true }, |
| 1775 | ); | 1934 | ); |
| 1776 | const point = yield* resolveClickPoint(send, input); | 1935 | const point = yield* resolveClickPoint(tabId, send, input); |
| 1777 | const viewport = yield* evaluateWithDebugger<{ width: number; height: number }>( | 1936 | const viewport = yield* evaluateWithDebugger<{ width: number; height: number }>( |
| | 1937 | tabId, |
| 1778 | send, | 1938 | send, |
| 1779 | "({ width: window.innerWidth, height: window.innerHeight })", | 1939 | "({ width: window.innerWidth, height: window.innerHeight })", |
| 1780 | true, | 1940 | true, |
| 1781 | ); | 1941 | ); |
| 1782 | if (point.x < 0 || point.y < 0 || point.x > viewport.width || point.y > viewport.height) { | 1942 | if (point.x < 0 || point.y < 0 || point.x > viewport.width || point.y > viewport.height) { |
| 1783 | return yield* fail( | 1943 | return yield* new PreviewAutomationCoordinatesOutsideViewportError({ |
| 1784 | "automationClick", | 1944 | tabId, |
| 1785 | automationError( | 1945 | x: point.x, |
| 1786 | "PreviewAutomationExecutionError", | 1946 | y: point.y, |
| 1787 | `Click coordinates (${point.x}, ${point.y}) are outside the preview viewport.`, | 1947 | viewportWidth: viewport.width, |
| 1788 | ), | 1948 | viewportHeight: viewport.height, |
| 1789 | ); | 1949 | }); |
| 1790 | } | 1950 | } |
| 1791 | const moveSequence = yield* nextCounter(pointerSequenceRef); | 1951 | const moveSequence = yield* nextCounter(pointerSequenceRef); |
| 1792 | const moveCreatedAt = yield* currentIso; | 1952 | const moveCreatedAt = yield* currentIso; |
| ... | @@ -1834,15 +1994,19 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1834,15 +1994,19 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1834 | }); | 1994 | }); |
| 1835 | | 1995 | |
| 1836 | const focusAutomationTarget = Effect.fn("PreviewManager.focusAutomationTarget")(function* ( | 1996 | const focusAutomationTarget = Effect.fn("PreviewManager.focusAutomationTarget")(function* ( |
| | 1997 | tabId: string, |
| 1837 | send: SendCommand, | 1998 | send: SendCommand, |
| 1838 | input: PreviewAutomationTypeInput, | 1999 | input: PreviewAutomationTypeInput, |
| 1839 | ) { | 2000 | ) { |
| 1840 | const locator = automationLocator(input); | 2001 | const locator = automationLocator(input); |
| 1841 | if (locator) yield* ensurePlaywrightInjected(send); | 2002 | if (locator) yield* ensurePlaywrightInjected(tabId, send); |
| 1842 | const locatorJson = locator ? yield* encodeJson("automationType.encodeLocator", locator) : null; | 2003 | const locatorJson = locator |
| | 2004 | ? yield* encodeJson({ operation: "automationType.encodeLocator", tabId }, locator) |
| | 2005 | : null; |
| 1843 | const result = yield* evaluateWithDebugger< | 2006 | const result = yield* evaluateWithDebugger< |
| 1844 | { ok: true } | { invalidSelector: true; message: string } | { notFound: true } | 2007 | { ok: true } | { invalidSelector: true; message: string } | { notFound: true } |
| 1845 | >( | 2008 | >( |
| | 2009 | tabId, |
| 1846 | send, | 2010 | send, |
| 1847 | `(() => { | 2011 | `(() => { |
| 1848 | try { | 2012 | try { |
| ... | @@ -1862,23 +2026,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1862,23 +2026,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1862 | true, | 2026 | true, |
| 1863 | ); | 2027 | ); |
| 1864 | if ("invalidSelector" in result) { | 2028 | if ("invalidSelector" in result) { |
| 1865 | return yield* fail( | 2029 | return yield* new PreviewAutomationInvalidSelectorError({ |
| 1866 | "automationType", | 2030 | operation: "type", |
| 1867 | automationError("PreviewAutomationInvalidSelectorError", result.message, { | 2031 | tabId, |
| 1868 | selector: input.selector ?? "", | 2032 | ...automationSelectorDiagnostics(input), |
| 1869 | }), | 2033 | reasonLength: result.message.length, |
| 1870 | ); | 2034 | cause: result, |
| | 2035 | }); |
| 1871 | } | 2036 | } |
| 1872 | if ("notFound" in result) { | 2037 | if ("notFound" in result) { |
| 1873 | return yield* fail( | 2038 | return yield* new PreviewAutomationTargetNotFoundError({ |
| 1874 | "automationType", | 2039 | operation: "type", |
| 1875 | automationError( | 2040 | tabId, |
| 1876 | "PreviewAutomationExecutionError", | 2041 | ...automationSelectorDiagnostics(input), |
| 1877 | locator | 2042 | }); |
| 1878 | ? `No element matches locator ${locator}.` | | |
| 1879 | : "No element is focused in the preview.", | | |
| 1880 | ), | | |
| 1881 | ); | | |
| 1882 | } | 2043 | } |
| 1883 | }); | 2044 | }); |
| 1884 | | 2045 | |
| ... | @@ -1888,10 +2049,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1888,10 +2049,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1888 | send: SendCommand, | 2049 | send: SendCommand, |
| 1889 | ) { | 2050 | ) { |
| 1890 | yield* send("Runtime.enable"); | 2051 | yield* send("Runtime.enable"); |
| 1891 | yield* focusAutomationTarget(send, input); | 2052 | yield* focusAutomationTarget(tabId, send, input); |
| 1892 | yield* send("Input.insertText", { text: input.text }); | 2053 | yield* send("Input.insertText", { text: input.text }); |
| 1893 | const textJson = yield* encodeJson("automationType.encodeText", input.text); | 2054 | const textJson = yield* encodeJson( |
| | 2055 | { operation: "automationType.encodeText", tabId }, |
| | 2056 | input.text, |
| | 2057 | ); |
| 1894 | yield* evaluateWithDebugger( | 2058 | yield* evaluateWithDebugger( |
| | 2059 | tabId, |
| 1895 | send, | 2060 | send, |
| 1896 | `(() => { | 2061 | `(() => { |
| 1897 | const element = document.activeElement; | 2062 | const element = document.activeElement; |
| ... | @@ -1959,13 +2124,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1959,13 +2124,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1959 | ) { | 2124 | ) { |
| 1960 | yield* send("Runtime.enable"); | 2125 | yield* send("Runtime.enable"); |
| 1961 | const locator = automationLocator(input); | 2126 | const locator = automationLocator(input); |
| 1962 | if (locator) yield* ensurePlaywrightInjected(send); | 2127 | if (locator) yield* ensurePlaywrightInjected(tabId, send); |
| 1963 | const locatorJson = locator | 2128 | const locatorJson = locator |
| 1964 | ? yield* encodeJson("automationScroll.encodeLocator", locator) | 2129 | ? yield* encodeJson({ operation: "automationScroll.encodeLocator", tabId }, locator) |
| 1965 | : null; | 2130 | : null; |
| 1966 | const result = yield* evaluateWithDebugger< | 2131 | const result = yield* evaluateWithDebugger< |
| 1967 | { ok: true } | { invalidSelector: true; message: string } | { notFound: true } | 2132 | { ok: true } | { invalidSelector: true; message: string } | { notFound: true } |
| 1968 | >( | 2133 | >( |
| | 2134 | tabId, |
| 1969 | send, | 2135 | send, |
| 1970 | `(() => { | 2136 | `(() => { |
| 1971 | try { | 2137 | try { |
| ... | @@ -1980,21 +2146,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -1980,21 +2146,20 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 1980 | true, | 2146 | true, |
| 1981 | ); | 2147 | ); |
| 1982 | if ("invalidSelector" in result) { | 2148 | if ("invalidSelector" in result) { |
| 1983 | return yield* fail( | 2149 | return yield* new PreviewAutomationInvalidSelectorError({ |
| 1984 | "automationScroll", | 2150 | operation: "scroll", |
| 1985 | automationError("PreviewAutomationInvalidSelectorError", result.message, { | 2151 | tabId, |
| 1986 | selector: input.selector ?? "", | 2152 | ...automationSelectorDiagnostics(input), |
| 1987 | }), | 2153 | reasonLength: result.message.length, |
| 1988 | ); | 2154 | cause: result, |
| | 2155 | }); |
| 1989 | } | 2156 | } |
| 1990 | if ("notFound" in result) { | 2157 | if ("notFound" in result) { |
| 1991 | return yield* fail( | 2158 | return yield* new PreviewAutomationTargetNotFoundError({ |
| 1992 | "automationScroll", | 2159 | operation: "scroll", |
| 1993 | automationError( | 2160 | tabId, |
| 1994 | "PreviewAutomationExecutionError", | 2161 | ...automationSelectorDiagnostics(input), |
| 1995 | `No element matches locator ${locator}.`, | 2162 | }); |
| 1996 | ), | | |
| 1997 | ); | | |
| 1998 | } | 2163 | } |
| 1999 | }); | 2164 | }); |
| 2000 | | 2165 | |
| ... | @@ -2009,24 +2174,26 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -2009,24 +2174,26 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 2009 | }); | 2174 | }); |
| 2010 | | 2175 | |
| 2011 | const performAutomationEvaluate = Effect.fn("PreviewManager.performAutomationEvaluate")( | 2176 | const performAutomationEvaluate = Effect.fn("PreviewManager.performAutomationEvaluate")( |
| 2012 | function* (input: PreviewAutomationEvaluateInput, send: SendCommand) { | 2177 | function* (tabId: string, input: PreviewAutomationEvaluateInput, send: SendCommand) { |
| 2013 | yield* send("Runtime.enable"); | 2178 | yield* send("Runtime.enable"); |
| 2014 | const value = yield* evaluateWithDebugger( | 2179 | const value = yield* evaluateWithDebugger( |
| | 2180 | tabId, |
| 2015 | send, | 2181 | send, |
| 2016 | input.expression, | 2182 | input.expression, |
| 2017 | input.returnByValue ?? true, | 2183 | input.returnByValue ?? true, |
| 2018 | input.awaitPromise ?? true, | 2184 | input.awaitPromise ?? true, |
| 2019 | ); | 2185 | ); |
| 2020 | const serialized = yield* encodeJson("automationEvaluate.encodeResult", value); | 2186 | const serialized = yield* encodeJson( |
| 2021 | if (Buffer.byteLength(serialized, "utf8") > MAX_EVALUATION_BYTES) { | 2187 | { operation: "automationEvaluate.encodeResult", tabId }, |
| 2022 | return yield* fail( | 2188 | value, |
| 2023 | "automationEvaluate", | 2189 | ); |
| 2024 | automationError( | 2190 | const actualBytes = Buffer.byteLength(serialized, "utf8"); |
| 2025 | "PreviewAutomationResultTooLargeError", | 2191 | if (actualBytes > MAX_EVALUATION_BYTES) { |
| 2026 | `Evaluation result exceeds ${MAX_EVALUATION_BYTES} bytes.`, | 2192 | return yield* new PreviewAutomationResultTooLargeError({ |
| 2027 | { maximumBytes: MAX_EVALUATION_BYTES }, | 2193 | tabId, |
| 2028 | ), | 2194 | actualBytes, |
| 2029 | ); | 2195 | maximumBytes: MAX_EVALUATION_BYTES, |
| | 2196 | }); |
| 2030 | } | 2197 | } |
| 2031 | return value; | 2198 | return value; |
| 2032 | }, | 2199 | }, |
| ... | @@ -2038,23 +2205,28 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -2038,23 +2205,28 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 2038 | ) { | 2205 | ) { |
| 2039 | const wc = yield* requireWebContents(tabId); | 2206 | const wc = yield* requireWebContents(tabId); |
| 2040 | return yield* withControlSession(tabId, wc, "evaluate", (send) => | 2207 | return yield* withControlSession(tabId, wc, "evaluate", (send) => |
| 2041 | performAutomationEvaluate(input, send), | 2208 | performAutomationEvaluate(tabId, input, send), |
| 2042 | ); | 2209 | ); |
| 2043 | }); | 2210 | }); |
| 2044 | | 2211 | |
| 2045 | const performAutomationWaitFor = Effect.fn("PreviewManager.performAutomationWaitFor")(function* ( | 2212 | const performAutomationWaitFor = Effect.fn("PreviewManager.performAutomationWaitFor")(function* ( |
| | 2213 | tabId: string, |
| 2046 | input: PreviewAutomationWaitForInput, | 2214 | input: PreviewAutomationWaitForInput, |
| 2047 | send: SendCommand, | 2215 | send: SendCommand, |
| 2048 | ) { | 2216 | ) { |
| 2049 | const timeoutMs = input.timeoutMs ?? 15_000; | 2217 | const timeoutMs = input.timeoutMs ?? 15_000; |
| 2050 | yield* send("Runtime.enable"); | 2218 | yield* send("Runtime.enable"); |
| 2051 | const locator = automationLocator(input); | 2219 | const locator = automationLocator(input); |
| 2052 | if (locator) yield* ensurePlaywrightInjected(send); | 2220 | if (locator) yield* ensurePlaywrightInjected(tabId, send); |
| 2053 | const [locatorJson, textJson, urlIncludesJson] = yield* Effect.all([ | 2221 | const [locatorJson, textJson, urlIncludesJson] = yield* Effect.all([ |
| 2054 | locator ? encodeJson("automationWaitFor.encodeLocator", locator) : Effect.succeed(null), | 2222 | locator |
| 2055 | input.text ? encodeJson("automationWaitFor.encodeText", input.text) : Effect.succeed(null), | 2223 | ? encodeJson({ operation: "automationWaitFor.encodeLocator", tabId }, locator) |
| | 2224 | : Effect.succeed(null), |
| | 2225 | input.text |
| | 2226 | ? encodeJson({ operation: "automationWaitFor.encodeText", tabId }, input.text) |
| | 2227 | : Effect.succeed(null), |
| 2056 | input.urlIncludes | 2228 | input.urlIncludes |
| 2057 | ? encodeJson("automationWaitFor.encodeUrl", input.urlIncludes) | 2229 | ? encodeJson({ operation: "automationWaitFor.encodeUrl", tabId }, input.urlIncludes) |
| 2058 | : Effect.succeed(null), | 2230 | : Effect.succeed(null), |
| 2059 | ]); | 2231 | ]); |
| 2060 | const deadline = (yield* currentMillis) + timeoutMs; | 2232 | const deadline = (yield* currentMillis) + timeoutMs; |
| ... | @@ -2062,6 +2234,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -2062,6 +2234,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 2062 | const result = yield* evaluateWithDebugger< | 2234 | const result = yield* evaluateWithDebugger< |
| 2063 | { matched: boolean } | { invalidSelector: true; message: string } | 2235 | { matched: boolean } | { invalidSelector: true; message: string } |
| 2064 | >( | 2236 | >( |
| | 2237 | tabId, |
| 2065 | send, | 2238 | send, |
| 2066 | `(() => { | 2239 | `(() => { |
| 2067 | try { | 2240 | try { |
| ... | @@ -2080,23 +2253,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -2080,23 +2253,21 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 2080 | true, | 2253 | true, |
| 2081 | ); | 2254 | ); |
| 2082 | if ("invalidSelector" in result) { | 2255 | if ("invalidSelector" in result) { |
| 2083 | return yield* fail( | 2256 | return yield* new PreviewAutomationInvalidSelectorError({ |
| 2084 | "automationWaitFor", | 2257 | operation: "waitFor", |
| 2085 | automationError("PreviewAutomationInvalidSelectorError", result.message, { | 2258 | tabId, |
| 2086 | selector: input.selector ?? "", | 2259 | ...automationSelectorDiagnostics(input), |
| 2087 | }), | 2260 | reasonLength: result.message.length, |
| 2088 | ); | 2261 | cause: result, |
| | 2262 | }); |
| 2089 | } | 2263 | } |
| 2090 | if (result.matched) return; | 2264 | if (result.matched) return; |
| 2091 | yield* Effect.sleep(100); | 2265 | yield* Effect.sleep(100); |
| 2092 | } | 2266 | } |
| 2093 | return yield* fail( | 2267 | return yield* new PreviewAutomationTimeoutError({ |
| 2094 | "automationWaitFor", | 2268 | tabId, |
| 2095 | automationError( | 2269 | timeoutMs, |
| 2096 | "PreviewAutomationTimeoutError", | 2270 | }); |
| 2097 | `Preview condition did not match within ${timeoutMs}ms.`, | | |
| 2098 | ), | | |
| 2099 | ); | | |
| 2100 | }); | 2271 | }); |
| 2101 | | 2272 | |
| 2102 | const automationWaitFor = Effect.fn("PreviewManager.automationWaitFor")(function* ( | 2273 | const automationWaitFor = Effect.fn("PreviewManager.automationWaitFor")(function* ( |
| ... | @@ -2105,7 +2276,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -2105,7 +2276,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 2105 | ) { | 2276 | ) { |
| 2106 | const wc = yield* requireWebContents(tabId); | 2277 | const wc = yield* requireWebContents(tabId); |
| 2107 | yield* withControlSession(tabId, wc, "waitFor", (send) => | 2278 | yield* withControlSession(tabId, wc, "waitFor", (send) => |
| 2108 | performAutomationWaitFor(input, send), | 2279 | performAutomationWaitFor(tabId, input, send), |
| 2109 | ); | 2280 | ); |
| 2110 | }); | 2281 | }); |
| 2111 | | 2282 | |
| ... | @@ -2113,23 +2284,25 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function | ... | @@ -2113,23 +2284,25 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function |
| 2113 | artifactPath: string, | 2284 | artifactPath: string, |
| 2114 | ) { | 2285 | ) { |
| 2115 | const resolvedPath = yield* resolveArtifactPath(artifactPath); | 2286 | const resolvedPath = yield* resolveArtifactPath(artifactPath); |
| 2116 | yield* attempt("revealArtifact", () => shell.showItemInFolder(resolvedPath)); | 2287 | yield* attempt({ operation: "revealArtifact", artifactPath: resolvedPath }, () => |
| | 2288 | shell.showItemInFolder(resolvedPath), |
| | 2289 | ); |
| 2117 | }); | 2290 | }); |
| 2118 | | 2291 | |
| 2119 | const copyArtifactToClipboard = Effect.fn("PreviewManager.copyArtifactToClipboard")(function* ( | 2292 | const copyArtifactToClipboard = Effect.fn("PreviewManager.copyArtifactToClipboard")(function* ( |
| 2120 | artifactPath: string, | 2293 | artifactPath: string, |
| 2121 | ) { | 2294 | ) { |
| 2122 | const resolvedPath = yield* resolveArtifactPath(artifactPath); | 2295 | const resolvedPath = yield* resolveArtifactPath(artifactPath); |
| 2123 | const image = yield* attempt("copyArtifactToClipboard.load", () => | 2296 | const image = yield* attempt( |
| 2124 | nativeImage.createFromPath(resolvedPath), | 2297 | { operation: "copyArtifactToClipboard.load", artifactPath: resolvedPath }, |
| | 2298 | () => nativeImage.createFromPath(resolvedPath), |
| 2125 | ); | 2299 | ); |
| 2126 | if (image.isEmpty()) { | 2300 | if (image.isEmpty()) { |
| 2127 | return yield* fail( | 2301 | return yield* new PreviewArtifactImageLoadError({ artifactPath: resolvedPath }); |
| 2128 | "copyArtifactToClipboard", | | |
| 2129 | new Error("Preview artifact could not be loaded as an image."), | | |
| 2130 | ); | | |
| 2131 | } | 2302 | } |
| 2132 | yield* attempt("copyArtifactToClipboard.write", () => clipboard.writeImage(image)); | 2303 | yield* attempt({ operation: "copyArtifactToClipboard.write", artifactPath: resolvedPath }, () => |
| | 2304 | clipboard.writeImage(image), |
| | 2305 | ); |
| 2133 | }); | 2306 | }); |
| 2134 | | 2307 | |
| 2135 | const subscribe = <A>( | 2308 | const subscribe = <A>( |
| ... | @@ -2228,19 +2401,234 @@ export class PreviewWebviewNotInitializedError extends Schema.TaggedErrorClass<P | ... | @@ -2228,19 +2401,234 @@ export class PreviewWebviewNotInitializedError extends Schema.TaggedErrorClass<P |
| 2228 | } | 2401 | } |
| 2229 | } | 2402 | } |
| 2230 | | 2403 | |
| 2231 | export class PreviewManagerError extends Schema.TaggedErrorClass<PreviewManagerError>()( | 2404 | export class PreviewOperationError extends Schema.TaggedErrorClass<PreviewOperationError>()( |
| 2232 | "PreviewManagerError", | 2405 | "PreviewOperationError", |
| 2233 | { | 2406 | { |
| 2234 | operation: Schema.String, | 2407 | operation: Schema.String, |
| | 2408 | tabId: Schema.optional(Schema.String), |
| | 2409 | webContentsId: Schema.optional(Schema.Number), |
| | 2410 | artifactPath: Schema.optional(Schema.String), |
| 2235 | cause: Schema.Defect(), | 2411 | cause: Schema.Defect(), |
| 2236 | }, | 2412 | }, |
| 2237 | ) { | 2413 | ) { |
| | 2414 | static toTimelineMessage(error: PreviewOperationError): string { |
| | 2415 | return error.cause instanceof Error ? error.cause.message : String(error.cause); |
| | 2416 | } |
| | 2417 | |
| 2238 | override get message(): string { | 2418 | override get message(): string { |
| 2239 | return `Desktop preview operation failed: ${this.operation}`; | 2419 | const context = [ |
| | 2420 | this.tabId === undefined ? undefined : `tab ${this.tabId}`, |
| | 2421 | this.webContentsId === undefined ? undefined : `WebContents ${this.webContentsId}`, |
| | 2422 | this.artifactPath === undefined ? undefined : `artifact ${this.artifactPath}`, |
| | 2423 | ].filter((value): value is string => value !== undefined); |
| | 2424 | return `Desktop preview operation failed: ${this.operation}${context.length === 0 ? "" : ` (${context.join(", ")})`}`; |
| 2240 | } | 2425 | } |
| 2241 | } | 2426 | } |
| 2242 | | 2427 | |
| 2243 | const isPreviewManagerError = Schema.is(PreviewManagerError); | 2428 | export const isPreviewOperationError = Schema.is(PreviewOperationError); |
| | 2429 | |
| | 2430 | export class PreviewArtifactPathOutsideDirectoryError extends Schema.TaggedErrorClass<PreviewArtifactPathOutsideDirectoryError>()( |
| | 2431 | "PreviewArtifactPathOutsideDirectoryError", |
| | 2432 | { |
| | 2433 | artifactPath: Schema.String, |
| | 2434 | artifactDirectory: Schema.String, |
| | 2435 | }, |
| | 2436 | ) { |
| | 2437 | override get message(): string { |
| | 2438 | return `Preview artifact path ${this.artifactPath} is outside ${this.artifactDirectory}`; |
| | 2439 | } |
| | 2440 | } |
| | 2441 | |
| | 2442 | export class PreviewArtifactImageLoadError extends Schema.TaggedErrorClass<PreviewArtifactImageLoadError>()( |
| | 2443 | "PreviewArtifactImageLoadError", |
| | 2444 | { artifactPath: Schema.String }, |
| | 2445 | ) { |
| | 2446 | override get message(): string { |
| | 2447 | return `Preview artifact could not be loaded as an image: ${this.artifactPath}`; |
| | 2448 | } |
| | 2449 | } |
| | 2450 | |
| | 2451 | export class PreviewRecordingAlreadyActiveError extends Schema.TaggedErrorClass<PreviewRecordingAlreadyActiveError>()( |
| | 2452 | "PreviewRecordingAlreadyActiveError", |
| | 2453 | { |
| | 2454 | requestedTabId: Schema.String, |
| | 2455 | activeTabId: Schema.String, |
| | 2456 | }, |
| | 2457 | ) { |
| | 2458 | override get message(): string { |
| | 2459 | return `Cannot record preview tab ${this.requestedTabId} while tab ${this.activeTabId} is already recording`; |
| | 2460 | } |
| | 2461 | } |
| | 2462 | |
| | 2463 | export class PreviewAutomationDevToolsOpenError extends Schema.TaggedErrorClass<PreviewAutomationDevToolsOpenError>()( |
| | 2464 | "PreviewAutomationDevToolsOpenError", |
| | 2465 | { webContentsId: Schema.Number }, |
| | 2466 | ) { |
| | 2467 | override get message(): string { |
| | 2468 | return `Close preview DevTools before using agent browser control for WebContents ${this.webContentsId}`; |
| | 2469 | } |
| | 2470 | } |
| | 2471 | |
| | 2472 | export class PreviewAutomationDebuggerAttachedError extends Schema.TaggedErrorClass<PreviewAutomationDebuggerAttachedError>()( |
| | 2473 | "PreviewAutomationDebuggerAttachedError", |
| | 2474 | { webContentsId: Schema.Number }, |
| | 2475 | ) { |
| | 2476 | override get message(): string { |
| | 2477 | return `Preview control cannot attach to WebContents ${this.webContentsId} because another debugger owns it`; |
| | 2478 | } |
| | 2479 | } |
| | 2480 | |
| | 2481 | export class PreviewAutomationEvaluationError extends Schema.TaggedErrorClass<PreviewAutomationEvaluationError>()( |
| | 2482 | "PreviewAutomationEvaluationError", |
| | 2483 | { |
| | 2484 | tabId: Schema.String, |
| | 2485 | detailKind: PreviewAutomationEvaluationDetailKind, |
| | 2486 | detailLength: Schema.Number, |
| | 2487 | cause: Schema.Defect(), |
| | 2488 | }, |
| | 2489 | ) { |
| | 2490 | static toTimelineMessage(error: PreviewAutomationEvaluationError): string { |
| | 2491 | return previewAutomationEvaluationDetail(error.cause).detail ?? error.message; |
| | 2492 | } |
| | 2493 | |
| | 2494 | override get message(): string { |
| | 2495 | return `Preview JavaScript evaluation failed in tab ${this.tabId}`; |
| | 2496 | } |
| | 2497 | } |
| | 2498 | |
| | 2499 | export class PreviewAutomationTargetNotFoundError extends Schema.TaggedErrorClass<PreviewAutomationTargetNotFoundError>()( |
| | 2500 | "PreviewAutomationTargetNotFoundError", |
| | 2501 | { |
| | 2502 | operation: Schema.String, |
| | 2503 | tabId: Schema.String, |
| | 2504 | selectorKind: PreviewAutomationSelectorKind, |
| | 2505 | selectorLength: Schema.optionalKey(Schema.Number), |
| | 2506 | }, |
| | 2507 | ) { |
| | 2508 | override get message(): string { |
| | 2509 | const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength); |
| | 2510 | return `Preview automation ${this.operation} could not find ${target} in tab ${this.tabId}`; |
| | 2511 | } |
| | 2512 | } |
| | 2513 | |
| | 2514 | export class PreviewAutomationCoordinatesOutsideViewportError extends Schema.TaggedErrorClass<PreviewAutomationCoordinatesOutsideViewportError>()( |
| | 2515 | "PreviewAutomationCoordinatesOutsideViewportError", |
| | 2516 | { |
| | 2517 | tabId: Schema.String, |
| | 2518 | x: Schema.Number, |
| | 2519 | y: Schema.Number, |
| | 2520 | viewportWidth: Schema.Number, |
| | 2521 | viewportHeight: Schema.Number, |
| | 2522 | }, |
| | 2523 | ) { |
| | 2524 | override get message(): string { |
| | 2525 | return `Click coordinates (${this.x}, ${this.y}) are outside the ${this.viewportWidth}x${this.viewportHeight} preview viewport for tab ${this.tabId}`; |
| | 2526 | } |
| | 2527 | } |
| | 2528 | |
| | 2529 | export class PreviewAutomationInvalidSelectorError extends Schema.TaggedErrorClass<PreviewAutomationInvalidSelectorError>()( |
| | 2530 | "PreviewAutomationInvalidSelectorError", |
| | 2531 | { |
| | 2532 | operation: Schema.String, |
| | 2533 | tabId: Schema.String, |
| | 2534 | selectorKind: PreviewAutomationSelectorKind, |
| | 2535 | selectorLength: Schema.optionalKey(Schema.Number), |
| | 2536 | reasonLength: Schema.Number, |
| | 2537 | cause: Schema.Defect(), |
| | 2538 | }, |
| | 2539 | ) { |
| | 2540 | static toTimelineMessage(error: PreviewAutomationInvalidSelectorError): string { |
| | 2541 | if (typeof error.cause !== "object" || error.cause === null) return error.message; |
| | 2542 | const reason = (error.cause as Record<string, unknown>)["message"]; |
| | 2543 | return typeof reason === "string" && reason.length > 0 ? reason : error.message; |
| | 2544 | } |
| | 2545 | |
| | 2546 | get detail(): { |
| | 2547 | readonly selectorKind: PreviewAutomationSelectorKind; |
| | 2548 | readonly selectorLength?: number; |
| | 2549 | } { |
| | 2550 | return { |
| | 2551 | selectorKind: this.selectorKind, |
| | 2552 | ...(this.selectorLength === undefined ? {} : { selectorLength: this.selectorLength }), |
| | 2553 | }; |
| | 2554 | } |
| | 2555 | |
| | 2556 | override get message(): string { |
| | 2557 | const target = previewAutomationTargetLabel(this.selectorKind, this.selectorLength); |
| | 2558 | return `Preview automation ${this.operation} rejected ${target} in tab ${this.tabId}`; |
| | 2559 | } |
| | 2560 | } |
| | 2561 | |
| | 2562 | export class PreviewAutomationResultTooLargeError extends Schema.TaggedErrorClass<PreviewAutomationResultTooLargeError>()( |
| | 2563 | "PreviewAutomationResultTooLargeError", |
| | 2564 | { |
| | 2565 | tabId: Schema.String, |
| | 2566 | actualBytes: Schema.Number, |
| | 2567 | maximumBytes: Schema.Number, |
| | 2568 | }, |
| | 2569 | ) { |
| | 2570 | get detail(): { readonly maximumBytes: number } { |
| | 2571 | return { maximumBytes: this.maximumBytes }; |
| | 2572 | } |
| | 2573 | |
| | 2574 | override get message(): string { |
| | 2575 | return `Preview evaluation result in tab ${this.tabId} was ${this.actualBytes} bytes; maximum is ${this.maximumBytes} bytes`; |
| | 2576 | } |
| | 2577 | } |
| | 2578 | |
| | 2579 | export class PreviewAutomationTimeoutError extends Schema.TaggedErrorClass<PreviewAutomationTimeoutError>()( |
| | 2580 | "PreviewAutomationTimeoutError", |
| | 2581 | { |
| | 2582 | tabId: Schema.String, |
| | 2583 | timeoutMs: Schema.Number, |
| | 2584 | }, |
| | 2585 | ) { |
| | 2586 | override get message(): string { |
| | 2587 | return `Preview condition did not match within ${this.timeoutMs}ms in tab ${this.tabId}`; |
| | 2588 | } |
| | 2589 | } |
| | 2590 | |
| | 2591 | export class PreviewAutomationControlInterruptedError extends Schema.TaggedErrorClass<PreviewAutomationControlInterruptedError>()( |
| | 2592 | "PreviewAutomationControlInterruptedError", |
| | 2593 | { |
| | 2594 | operation: Schema.String, |
| | 2595 | tabId: Schema.String, |
| | 2596 | webContentsId: Schema.Number, |
| | 2597 | }, |
| | 2598 | ) { |
| | 2599 | override get message(): string { |
| | 2600 | return `Preview automation ${this.operation} was interrupted by human input in tab ${this.tabId}`; |
| | 2601 | } |
| | 2602 | } |
| | 2603 | |
| | 2604 | export const PreviewManagerError = Schema.Union([ |
| | 2605 | PreviewTabNotFoundError, |
| | 2606 | PreviewWebContentsNotFoundError, |
| | 2607 | PreviewWebviewNotInitializedError, |
| | 2608 | PreviewOperationError, |
| | 2609 | PreviewArtifactPathOutsideDirectoryError, |
| | 2610 | PreviewArtifactImageLoadError, |
| | 2611 | PreviewRecordingAlreadyActiveError, |
| | 2612 | PreviewAutomationDevToolsOpenError, |
| | 2613 | PreviewAutomationDebuggerAttachedError, |
| | 2614 | PreviewAutomationEvaluationError, |
| | 2615 | PreviewAutomationTargetNotFoundError, |
| | 2616 | PreviewAutomationCoordinatesOutsideViewportError, |
| | 2617 | PreviewAutomationInvalidSelectorError, |
| | 2618 | PreviewAutomationResultTooLargeError, |
| | 2619 | PreviewAutomationTimeoutError, |
| | 2620 | PreviewAutomationControlInterruptedError, |
| | 2621 | ]); |
| | 2622 | export type PreviewManagerError = typeof PreviewManagerError.Type; |
| | 2623 | |
| | 2624 | export const isPreviewManagerError = Schema.is(PreviewManagerError); |
| | 2625 | export const isPreviewAutomationControlInterruptedError = Schema.is( |
| | 2626 | PreviewAutomationControlInterruptedError, |
| | 2627 | ); |
| | 2628 | export const isPreviewAutomationEvaluationError = Schema.is(PreviewAutomationEvaluationError); |
| | 2629 | export const isPreviewAutomationInvalidSelectorError = Schema.is( |
| | 2630 | PreviewAutomationInvalidSelectorError, |
| | 2631 | ); |
| 2244 | | 2632 | |
| 2245 | export class PreviewManager extends Context.Service< | 2633 | export class PreviewManager extends Context.Service< |
| 2246 | PreviewManager, | 2634 | PreviewManager, |
| ... | @@ -2329,16 +2717,17 @@ export const make = Effect.gen(function* PreviewManagerMake() { | ... | @@ -2329,16 +2717,17 @@ export const make = Effect.gen(function* PreviewManagerMake() { |
| 2329 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 2717 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 2330 | const browserSession = yield* BrowserSession.BrowserSession; | 2718 | const browserSession = yield* BrowserSession.BrowserSession; |
| 2331 | const operations = yield* makeNativeOperations(environment.browserArtifactsDir); | 2719 | const operations = yield* makeNativeOperations(environment.browserArtifactsDir); |
| 2332 | const browserSessionEffect = <A>( | | |
| 2333 | operation: string, | | |
| 2334 | effect: Effect.Effect<A, BrowserSession.BrowserSessionError>, | | |
| 2335 | ): Effect.Effect<A, PreviewManagerError> => | | |
| 2336 | effect.pipe(Effect.mapError((cause) => new PreviewManagerError({ operation, cause }))); | | |
| 2337 | | 2720 | |
| 2338 | return PreviewManager.of({ | 2721 | return PreviewManager.of({ |
| 2339 | setMainWindow: operations.setMainWindow, | 2722 | setMainWindow: operations.setMainWindow, |
| 2340 | getBrowserSession: Effect.fn("PreviewManager.getBrowserSession")(function* (scope) { | 2723 | getBrowserSession: Effect.fn("PreviewManager.getBrowserSession")(function* (scope) { |
| 2341 | return yield* browserSessionEffect("getBrowserSession", browserSession.getSession(scope)); | 2724 | return yield* browserSession |
| | 2725 | .getSession(scope) |
| | 2726 | .pipe( |
| | 2727 | Effect.mapError( |
| | 2728 | (cause) => new PreviewOperationError({ operation: "getBrowserSession", cause }), |
| | 2729 | ), |
| | 2730 | ); |
| 2342 | }), | 2731 | }), |
| 2343 | isBrowserPartition: browserSession.isPartition, | 2732 | isBrowserPartition: browserSession.isPartition, |
| 2344 | createTab: operations.createTab, | 2733 | createTab: operations.createTab, |
| ... | @@ -2354,13 +2743,29 @@ export const make = Effect.gen(function* PreviewManagerMake() { | ... | @@ -2354,13 +2743,29 @@ export const make = Effect.gen(function* PreviewManagerMake() { |
| 2354 | hardReload: operations.hardReload, | 2743 | hardReload: operations.hardReload, |
| 2355 | openDevTools: operations.openDevTools, | 2744 | openDevTools: operations.openDevTools, |
| 2356 | clearCookies: Effect.fn("PreviewManager.clearCookies")(function* () { | 2745 | clearCookies: Effect.fn("PreviewManager.clearCookies")(function* () { |
| 2357 | yield* browserSessionEffect("clearCookies", browserSession.clearCookies()); | 2746 | yield* browserSession |
| | 2747 | .clearCookies() |
| | 2748 | .pipe( |
| | 2749 | Effect.mapError( |
| | 2750 | (cause) => new PreviewOperationError({ operation: "clearCookies", cause }), |
| | 2751 | ), |
| | 2752 | ); |
| 2358 | }), | 2753 | }), |
| 2359 | clearCache: Effect.fn("PreviewManager.clearCache")(function* () { | 2754 | clearCache: Effect.fn("PreviewManager.clearCache")(function* () { |
| 2360 | yield* browserSessionEffect("clearCache", browserSession.clearCache()); | 2755 | yield* browserSession |
| | 2756 | .clearCache() |
| | 2757 | .pipe( |
| | 2758 | Effect.mapError((cause) => new PreviewOperationError({ operation: "clearCache", cause })), |
| | 2759 | ); |
| 2361 | }), | 2760 | }), |
| 2362 | getBrowserPartition: Effect.fn("PreviewManager.getBrowserPartition")(function* (scope) { | 2761 | getBrowserPartition: Effect.fn("PreviewManager.getBrowserPartition")(function* (scope) { |
| 2363 | return yield* browserSessionEffect("getBrowserPartition", browserSession.getPartition(scope)); | 2762 | return yield* browserSession |
| | 2763 | .getPartition(scope) |
| | 2764 | .pipe( |
| | 2765 | Effect.mapError( |
| | 2766 | (cause) => new PreviewOperationError({ operation: "getBrowserPartition", cause }), |
| | 2767 | ), |
| | 2768 | ); |
| 2364 | }), | 2769 | }), |
| 2365 | setAnnotationTheme: operations.setAnnotationTheme, | 2770 | setAnnotationTheme: operations.setAnnotationTheme, |
| 2366 | pickElement: operations.pickElement, | 2771 | pickElement: operations.pickElement, |