Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web fixes #2250

Merged
merged 7 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ object MaestroSessionManager {
deviceId: String?,
platform: Platform? = null,
): SelectedDevice {
if (deviceId == "chromium") {
if (deviceId == "chromium" || platform == Platform.WEB) {
return SelectedDevice(
platform = Platform.WEB
)
Expand Down
9 changes: 6 additions & 3 deletions maestro-client/src/main/java/maestro/drivers/WebDriver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,11 @@ class WebDriver(
return TreeNode(attributes = attributes, children = children.map { parse(it) })
}


return parse(contentDesc as Map<String, Any>)
val root = parse(contentDesc as Map<String, Any>)
seleniumDriver?.currentUrl?.let { url ->
root.attributes["url"] = url
}
return root
}

private fun detectWindowChange() {
Expand Down Expand Up @@ -367,7 +370,7 @@ class WebDriver(
override fun openLink(link: String, appId: String?, autoVerify: Boolean, browser: Boolean) {
val driver = ensureOpen()

driver.get(link)
driver.get(if (link.startsWith("http")) link else "https://$link")
}

override fun hideKeyboard() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ object DeviceService {
val deviceScreen = getDeviceScreen(maestro)
writeStringUtf8("data: $deviceScreen\n\n")
flush()
} catch (e: Exception) {
} catch (_: Exception) {
// Ignoring the exception to prevent SSE stream from dying
e.printStackTrace()
// Don't log since this floods the terminal after killing studio
}
}
}
Expand Down Expand Up @@ -187,8 +187,9 @@ object DeviceService {
val deviceWidth = deviceInfo.widthGrid
val deviceHeight = deviceInfo.heightGrid

val url = tree.attributes["url"]
val elements = treeToElements(tree)
val deviceScreen = DeviceScreen("/screenshot/${screenshotFile.name}", deviceWidth, deviceHeight, elements)
val deviceScreen = DeviceScreen(deviceInfo.platform, "/screenshot/${screenshotFile.name}", deviceWidth, deviceHeight, elements, url)
return jacksonObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.writeValueAsString(deviceScreen)
Expand Down
3 changes: 3 additions & 0 deletions maestro-studio/server/src/main/java/maestro/studio/Models.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package maestro.studio

import com.fasterxml.jackson.annotation.JsonProperty
import maestro.Platform
import java.util.UUID

data class DeviceScreen(
val platform: Platform,
val screenshot: String,
val width: Int,
val height: Int,
val elements: List<UIElement>,
val url: String?,
)

data class UIElementBounds(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, {useLayoutEffect} from "react";
import {twMerge} from "tailwind-merge";
import {Input} from "../design-system/input";

const GlobeIcon = ({ className }: { className?: string }) => (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 256 256"
>
<path
fill="currentColor"
d="M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"></path>
</svg>
);

const BrowserActionBar = ({currentUrl, onUrlUpdated, isLoading}: {
currentUrl?: string,
onUrlUpdated: (url: string) => void,
isLoading: boolean
}) => {
const [isEditing, setIsEditing] = React.useState(false)
const [editedUrl, setEditedUrl] = React.useState(currentUrl)
useLayoutEffect(() => {
if (!isEditing && !isLoading) {
setEditedUrl(currentUrl)
}
}, [isLoading, isEditing, currentUrl]);
return (
<div className="w-full relative">
<div className="inset-y-0 absolute flex items-center px-1.5">
<GlobeIcon className="text-gray-300" />
</div>
<Input
className={twMerge(
"w-full pl-8 pr-1 py-0.5 rounded-full border-2 bg-slate-50",
isLoading && "bg-gray-100",
)}
size="sm"
disabled={isLoading}
value={(isEditing || isLoading) ? editedUrl : currentUrl}
onChange={(e) => setEditedUrl(e.target.value)}
onFocus={() => setIsEditing(true)}
onBlur={() => setIsEditing(false)}
onKeyDown={(e) => {
if (e.key === 'Enter' && isEditing && editedUrl) {
onUrlUpdated(editedUrl);
e.currentTarget.blur();
setIsEditing(false);
}
}}
/>
</div>
)
}

export default BrowserActionBar
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import clsx from "clsx";

import InteractableDevice from "../device-and-device-elements/InteractableDevice";
Expand All @@ -12,6 +12,7 @@ import { useDeviceContext } from "../../context/DeviceContext";
import { Spinner } from "../design-system/spinner";
import { useRepl } from '../../context/ReplContext';
import { DeviceScreen } from "../../helpers/models";
import BrowserActionBar from "../device-and-device-elements/BrowserActionBar";

const InteractPageLayout = () => {
const {
Expand All @@ -24,6 +25,7 @@ const InteractPageLayout = () => {
const { runCommandYaml } = useRepl();

const [showElementsPanel, setShowElementsPanel] = useState<boolean>(false);
const [isUrlLoading, setIsUrlLoading] = useState<boolean>(false);

const onEdit = (example: CommandExample) => {
if (example.status === "unavailable") return;
Expand All @@ -44,6 +46,16 @@ const InteractPageLayout = () => {
await runCommandYaml(example.content);
};

const onUrlUpdated = (url: string) => {
setIsUrlLoading(true);
runCommandYaml(`openLink: ${url}`).finally(() => {
// Wait some time to update the url from the device screen
setTimeout(() => {
setIsUrlLoading(false);
}, 1000);
});
}

if (isLoading)
return (
<div className="flex items-center justify-center h-full">
Expand Down Expand Up @@ -76,6 +88,13 @@ const InteractPageLayout = () => {
Search Elements with Text or Id
</Button>
)}
{deviceScreen?.platform === 'WEB' && (
<BrowserActionBar
currentUrl={deviceScreen.url}
onUrlUpdated={onUrlUpdated}
isLoading={isUrlLoading}
/>
)}
<DeviceWrapperAspectRatio>
<InteractableDevice />
</DeviceWrapperAspectRatio>
Expand Down
2 changes: 2 additions & 0 deletions maestro-studio/web/src/helpers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export type UIElement = {
};

export type DeviceScreen = {
platform: string;
screenshot: string;
width: number;
height: number;
elements: UIElement[];
url?: string;
};

export type ReplCommandStatus =
Expand Down
41 changes: 41 additions & 0 deletions maestro-studio/web/src/storybook/BrowserActionBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import BrowserActionBar from "../components/device-and-device-elements/BrowserActionBar";
import {useEffect, useState} from "react";

export default {
title: "BrowserActionBar",
};

export const Main = () => {
const [currentUrl, setCurrentUrl] = useState<string>("https://google.com");
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
const interval = setInterval(() => {
setCurrentUrl(old => {
// if no scheme, add https
if (!old.startsWith("http")) {
old = "https://" + old;
}
const url = new URL(old);
url.searchParams.set("ts", Date.now().toString());
return url.toString();
});
}, 1000);
return () => clearInterval(interval);
}, []);
const onUrlUpdated = (url: string) => {
setIsLoading(true);
setTimeout(() => {
setCurrentUrl(url);
setIsLoading(false);
}, 1000);
}
return (
<div className="flex flex-col w-full">
<BrowserActionBar
currentUrl={currentUrl}
onUrlUpdated={onUrlUpdated}
isLoading={isLoading}
/>
</div>
);
};
Loading