From c5588f9fa3e793f11d4ae9c0c16bcb52c15980b2 Mon Sep 17 00:00:00 2001 From: EAGzzyCSL Date: Wed, 2 Sep 2026 20:08:46 +0800 Subject: [PATCH] fix(web-integration): reuse static UI context --- .../src/static/static-agent.ts | 8 ++++ .../web-integration/src/static/static-page.ts | 40 ++++++++++++++++++- .../tests/unit-test/static-page.test.ts | 29 +++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/web-integration/src/static/static-agent.ts b/packages/web-integration/src/static/static-agent.ts index 919e521e7c..bc23e35d66 100644 --- a/packages/web-integration/src/static/static-agent.ts +++ b/packages/web-integration/src/static/static-agent.ts @@ -1,10 +1,18 @@ import { Agent as PageAgent } from '@midscene/core/agent'; +import type { UIContext } from '@midscene/core'; import type StaticPage from './static-page'; export class StaticPageAgent extends PageAgent { + private readonly staticPage: StaticPage; + constructor(page: StaticPage) { // Disable report generation in browser environment to avoid Node.js fs module errors super(page, { generateReport: false }); + this.staticPage = page; this.dryMode = true; } + + override async getUIContext(): Promise { + return this.staticPage.getUIContext(); + } } diff --git a/packages/web-integration/src/static/static-page.ts b/packages/web-integration/src/static/static-page.ts index 340cc45ebf..e270df2e2d 100644 --- a/packages/web-integration/src/static/static-page.ts +++ b/packages/web-integration/src/static/static-page.ts @@ -4,6 +4,7 @@ import type { ScreenshotRef, UIContext, } from '@midscene/core'; +import { ScreenshotItem } from '@midscene/core'; import type { AbstractInterface } from '@midscene/core/device'; import { type InputPrimitives, @@ -25,9 +26,12 @@ type SerializedStaticScreenshot = { type StaticPageUIContext = Omit< UIContext, - 'deprecatedDpr' | 'screenshot' + 'deprecatedDpr' | 'screenshot' | 'screenshotSequence' > & { screenshot: UIContext['screenshot'] | SerializedStaticScreenshot; + screenshotSequence?: Array< + UIContext['screenshot'] | SerializedStaticScreenshot + >; }; function screenshotBase64FromContext( @@ -50,10 +54,38 @@ function screenshotBase64FromContext( ); } +function screenshotItemFromContext( + screenshot: StaticPageUIContext['screenshot'], +): ScreenshotItem { + if (screenshot instanceof ScreenshotItem) { + return screenshot; + } + + const capturedAt = + typeof screenshot.capturedAt === 'number' + ? screenshot.capturedAt + : Date.now(); + return ScreenshotItem.create( + screenshotBase64FromContext(screenshot), + capturedAt, + ); +} + +function normalizeUIContext(uiContext: StaticPageUIContext): UIContext { + return { + ...uiContext, + screenshot: screenshotItemFromContext(uiContext.screenshot), + screenshotSequence: uiContext.screenshotSequence?.map( + screenshotItemFromContext, + ), + }; +} + export default class StaticPage implements AbstractInterface { interfaceType = 'static'; private uiContext: StaticPageUIContext; + private normalizedUIContext?: UIContext; readonly inputPrimitives: InputPrimitives = { pointer: { tap: async () => ThrowNotImplemented('Tap'), @@ -78,6 +110,11 @@ export default class StaticPage implements AbstractInterface { this.uiContext = uiContext; } + getUIContext(): UIContext { + this.normalizedUIContext ??= normalizeUIContext(this.uiContext); + return this.normalizedUIContext; + } + actionSpace(): DeviceAction[] { // Return available actions for static page - they will throw "not implemented" errors when executed // but need to be available for planning phase @@ -175,5 +212,6 @@ export default class StaticPage implements AbstractInterface { updateContext(newContext: StaticPageUIContext): void { this.uiContext = newContext; + this.normalizedUIContext = undefined; } } diff --git a/packages/web-integration/tests/unit-test/static-page.test.ts b/packages/web-integration/tests/unit-test/static-page.test.ts index 1ec3f38b28..81aaaa0826 100644 --- a/packages/web-integration/tests/unit-test/static-page.test.ts +++ b/packages/web-integration/tests/unit-test/static-page.test.ts @@ -1,6 +1,6 @@ import { ScreenshotItem } from '@midscene/core'; import { describe, expect, it } from '@rstest/core'; -import { StaticPage } from '../../src/static'; +import { StaticPage, StaticPageAgent } from '../../src/static'; const screenshotBase64 = 'data:image/png;base64,abc123'; @@ -53,4 +53,31 @@ describe('StaticPage', () => { 'serialized reference without base64 data', ); }); + + it('lets StaticPageAgent reuse the prepared UI context', async () => { + const capturedAt = 123; + const page = new StaticPage( + createContext({ base64: screenshotBase64, capturedAt }), + ); + const agent = new StaticPageAgent(page); + + const context = await agent.getUIContext(); + + expect(context.shotSize).toEqual({ width: 800, height: 600 }); + expect(context.shrunkShotToLogicalRatio).toBe(1); + expect(context.screenshot).toBeInstanceOf(ScreenshotItem); + expect(context.screenshot.base64).toBe(screenshotBase64); + expect(context.screenshot.capturedAt).toBe(capturedAt); + + const updatedScreenshotBase64 = 'data:image/png;base64,updated'; + page.updateContext( + createContext({ base64: updatedScreenshotBase64, capturedAt: 456 }), + ); + + const updatedContext = await agent.getUIContext(); + + expect(updatedContext).not.toBe(context); + expect(updatedContext.screenshot.base64).toBe(updatedScreenshotBase64); + expect(updatedContext.screenshot.capturedAt).toBe(456); + }); });