-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: drop down of apps from app management #113
Conversation
…ce error needs to be fixed
…nd likely app payloads uploaded by saucectl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One issue I noticed: when I load a test record for the first time, I see the correct app name selected, but it switches to the wrong app name after a brief pause. Only happens for the first test record I load, however.
Screen.Recording.2024-09-11.at.2.44.58.PM.mov
for (let i = 0; i < loadedAppNames.length; i++) { | ||
optionElements.push(<VSCodeOption>{loadedAppNames[i]}</VSCodeOption>); | ||
if (loadedAppNames[i] == appName) { | ||
seenHistoryAppName = true; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more idiomatic to do this declaratively by mapping the items directly to a component. For example:
{steps.map((step) => (
<TestStep
key={`${step.testRecordId}-${step.index}`}
dispatch={dispatch}
language={state.language}
step={step}
/>
))}
@@ -68,6 +68,10 @@ | |||
row-gap: 8px; | |||
} | |||
|
|||
.app-not-loaded { | |||
color: grey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a --vscode-*
color that's better for this? This grey doesn't stand out in a light theme.
className={ | ||
optionElements.some( | ||
(option) => option.props.className === 'app-not-loaded', | ||
) | ||
? 'app-not-loaded' | ||
: '' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use the :has()
pseudo-class instead of this.
css like this:
.app-list:has(.app-not-loaded) {
color: gray;
}
Then just add the .app-list
class:
className={ | |
optionElements.some( | |
(option) => option.props.className === 'app-not-loaded', | |
) | |
? 'app-not-loaded' | |
: '' | |
} | |
className="app-list" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't quite have the functionality I want, instead of only greying out the "bad" app, when I'm on that history instance, it greys out every app I selected, and the "bad" app is not greyed out in the drop down list
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😕 the two implementations are behaving identically for me...
src/api/llm/http.ts
Outdated
const data = (await response.json()) as { items: AppInfo[] }; | ||
|
||
if (data && Array.isArray(data.items) && data.items.every(isAppInfo)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a strange mix of being defensive about the response data while making assumptions about it. Why assume the response has an items
property but then check its items are the correct type? I'd think you'd want to verify the entire shape of the response if you're going to at all. And then have a single type guard instead of checking isAppInfo
on all the items.
src/api/llm/http.ts
Outdated
); | ||
|
||
if (!response.ok) { | ||
throw new Error('Network response was not ok'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could include the status
and/or statusText
in the error.
src/editors/TestGenerationPanel.ts
Outdated
TestGenerationPanel.currentPanel.loadAppNames(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every time I load a test record from the history list, the same list of apps needs to be fetched...is that intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was intentional, I didn't want users to be confused why when they reload the screen, even for history, the app's don't match what's in app management. For example, if they load a history instance and realize the app has been deleted, they might re-upload their app and expect to see the app name list reflect that when re-opening the history instance. But if the downsides of calling that API isn't worth that scenario, I can only have it load when it isn't loading history.
src/types.ts
Outdated
@@ -162,7 +162,7 @@ export function isStoppedResponse(data: unknown): data is StoppedResponse { | |||
); | |||
} | |||
|
|||
export type Platform = 'Android' | 'iOS'; | |||
export type Platform = 'android' | 'ios'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change safe? Platform
is used in a few places: the test generation request and its also serialized as part of the test record. There'd be a mismatch here between test records stored before and after this change...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could update it so both upper and lower case are accepted. I tested all the functionality and nothing appeared broken. On the server side, I added a .lower() so that even platform types that remain upper case would work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the type for data that gets serialized/deserialized seems at best unnecessary at worst a foot gun. The type is the type, if you have data that doesn't conform to it, change the data not the type, imo. Adding to the type union is non-breaking so is safer than this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I added to the union. I viewed it more as the type we had was incorrect as when we get the platform from the API call it is all lower case, and I thought having to change it each time was inefficient. Especially since each word has a different uppercase structure so we can't just uppercase the first letter, which is why I didn't change the data. But I can change that if that's preferred.
@@ -78,6 +78,8 @@ export interface State { | |||
language: 'python' | 'java'; | |||
|
|||
tunnel: Tunnel; | |||
|
|||
loadedAppInfo: AppInfo[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe just call it apps
for brevity?
src/api/llm/http.ts
Outdated
); | ||
|
||
if (!response.ok) { | ||
throw new Error('Network response was not ok'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will be informative enough for the user or for us if get an error report. You see it in the error message at
'Failed to load app names. Please check your credentials and try again.', |
const appNameToPlatform = new Map<string, string>(); | ||
loadedAppInfo.forEach((item) => appNameToPlatform.set(item.name, item.kind)); | ||
let seenHistoryAppName = appName == ''; // if appName is empty, we don't need to add appName later | ||
const optionElements: JSX.Element[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great if the app names were sorted. I had a bit of trouble navigating through our relatively large list!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know! I'll add that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/api/llm/http.ts
Outdated
@@ -72,3 +72,28 @@ export async function sendUserRating( | |||
throw new Error('Unexpected status code: ' + resp.status); | |||
} | |||
} | |||
|
|||
export async function fetchAppNames(creds: Credentials) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Imho a misnomer, since you return more than just the names. Whenever I look at this, I expect a list of strings to be returned. Would recommend a different name 🙏
- Won't this return both VDC and RDC type apps? Are we fully supporting VDC now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexplischke are there apps which are RDC or VDC specific? Do you have an example app that is VDC only? I'd be happy to test uploading and filtering that out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, zips are simulators only. I'll filter those out for now, with a comment to remove for when VDC is enabled.
type: 'setAppName', | ||
value: { | ||
appName: appName as string, | ||
platformName: appInfo.kind as 'ios' | 'android', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If appName
isn't in the list of apps, the platform needs to come from state?
nvm, it'll error anyways, I guess
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, when I test it running an app no longer in app management I get an error that says the app isn't in app storage, so the error statement is properly informative.
src/api/llm/http.ts
Outdated
return data.items.filter( | ||
(item) => | ||
!('is_simulator' in item.metadata) || | ||
item.metadata.is_simulator === false, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be better to have this logic live outside the api definition. The api layer should simply model the api itself (its parameters, expected responses, etc).
src/types.ts
Outdated
id: string; | ||
name: string; | ||
kind: string; | ||
metadata: Map<unknown, unknown>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The defined type doesn't match the logic in the type guard; its a Map
here, but in the type guard function below, it checks that metadata
is an object.
Could define it a couple ways. Just define the metadata you want:
metadata: Map<unknown, unknown>; | |
metadata: { | |
is_simulator?: boolean; | |
}; |
The AppInfo
type is already a truncated version of the actual server response, may as well make metadata
the same and add as needed.
Or, if you want something as flexible as Map
:
metadata: Map<unknown, unknown>; | |
metadata: { [key: string]: any }; |
I prefer the former.
Also keep in mind, metadata
can be null
. We probably won't encounter that case, but getting the types wrong defeats whatever safety we may get from having them in the first place.
src/api/llm/http.ts
Outdated
item.metadata.is_simulator === false, | ||
); | ||
} else { | ||
throw new Error('Unexpected data format from API'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should log out what the response in that case was. Else I'd have to modify the code just to be able to troubleshoot what came back from the server 🔍
src/editors/TestGenerationPanel.ts
Outdated
toast.showError( | ||
'Failed to load app names. Please check your credentials and try again.', | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to surface the error to the user. That way they can act on it (wrong credentials, in which case they'd have an auth error) or file an issue with the error message. Otherwise they'd simply see a templated response but not that's wrong.
toast.showError( | |
'Failed to load app names. Please check your credentials and try again.', | |
); | |
toast.showError( | |
`Failed to load app names: ${errMsg(error)}.`, | |
); |
|
||
export interface Assertion { | ||
value: string; | ||
key: string; | ||
} | ||
|
||
export interface Platform { | ||
name: 'iOS' | 'Android'; | ||
name: 'ios' | 'android' | 'iOS' | 'Android'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may be hiding some relevant historic information.
Am I correct to assume that the previously stored records are using iOS
and Android
?
If I was someone new to code, and I see this new definition now, I don't know why we have the case differences. If you intend to keep these changes, it'd be helpful to the future-us to have an inline explanation 🙏
💡 I think there's a more important gotcha here though that introduces some future pitfalls though and worth a re-evaluation. You can see it in
name: testRecord.platform.toLowerCase() as 'android' | 'ios', |
Food for thought:
- Do we need to change our platform definition just because App Storage defines it differently? Or should we transform what's coming from App Storage to our case standard? This change would be more isolated and require no backend change on our side either.
- Should we change our definition to lower case? In which case the correct way would be to do a full data migration (storage.ts has a placeholder exactly for this use case). Is it a annoying? Yes, but that's the consequence of not massaging App Storage responses to our needs.
- We're in beta and can use it to our advantage! Let's say, much like above, that maybe we do want to lowercase our platform definition eventually. We can transform the data coming from App Storage to our needs now and schedule the changes for a lowercase switch for a
v1.0.0
release, not needing the data migration change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will update this PR to modify the API data to be upper case and add a new ticket to update our platform definitions to match the API and be lower case, as both are acceptable inputs as platform names into appium and that will allow for greater consistency.
…a to also be uppercase. Adding clearer error statements, moving filtering of simulator only apps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more thought: should we validate the app selection before generating the test? Let's say I load history test record and the app it tested is not in app storage. I modify the record, leaving the app as is, and try to generate a test. What error will the user see in this case? Will it be easily understandable/actionable? If not, since we know on the client side the generation will fail, should we validate and present a nice error message right away?
let apps = action.value; | ||
apps = apps.filter( | ||
(item) => | ||
item.metadata === null || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a wee bit confused, since according to the AppInfo
interface, metadata is always defined. Which is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since metadata might be null, though rare, I'll allow metadata to be null in the type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd vote for adding it to the interface. The downside is a few extra null checks, upside is an accurate and reliable type.
!('is_simulator' in item.metadata) || | ||
item.metadata.is_simulator === false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be simplified:
!('is_simulator' in item.metadata) || | |
item.metadata.is_simulator === false, | |
!item.metadata.is_simulator |
item.metadata.is_simulator === false, | ||
); | ||
apps.forEach((app) => { | ||
app.kind = app.kind === 'android' ? 'Android' : 'iOS'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my other comments w/r/t/ massaging the data at the HTTP client level or type guarding.
type: 'setAppName', | ||
value: { | ||
appName: appName as string, | ||
platformName: appInfo.kind as 'iOS' | 'Android', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are still forcing types onto the compiler though 😅 If there's a bug in
app.kind = app.kind === 'android' ? 'Android' : 'iOS'; |
Options
- One technique that we use in saucectl quite often, is to massage the data that we receive at the HTTP client level, which would be in fetchApps() in this case. You could return a type of AppInfo that's internal to us, with
platformName
being a native field of typePlatform
. - Add a function for type guarding.
AppKindToPlatform()
or some such. Might result in more 🍝 code over time though.
dispatch({ | ||
type: 'setAppName', | ||
value: { | ||
appName: appName as string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const appName: string = e.target.value as string; |
I believe that the title can be renamed from |
@mhan83 for the question about the error message, you get a device connection failed message very quickly, it looks like this |
Description
pull names of apps from user's app management. Remove platform option as this is included with app name from API. If history instance uses app no longer in app management, app name is greyed out.
Additional:
Added optional to tunnel name and owner as these are optional fields to avoid confusion.