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

feat: drop down of apps from app management #113

Merged
merged 20 commits into from
Sep 17, 2024
Merged

feat: drop down of apps from app management #113

merged 20 commits into from
Sep 17, 2024

Conversation

lenareed
Copy link
Contributor

@lenareed lenareed commented Sep 9, 2024

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.

@lenareed lenareed requested a review from a team as a code owner September 9, 2024 22:07
@lenareed lenareed changed the title Draft: drop down of apps from app management Draft: feat: drop down of apps from app management Sep 9, 2024
@lenareed lenareed changed the title Draft: feat: drop down of apps from app management feat!: drop down of apps from app management Sep 10, 2024
@lenareed lenareed changed the title feat!: drop down of apps from app management Draft: feat!: drop down of apps from app management Sep 10, 2024
@lenareed lenareed changed the title Draft: feat!: drop down of apps from app management feat!: drop down of apps from app management Sep 11, 2024
Copy link
Contributor

@mhan83 mhan83 left a 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

Comment on lines 132 to 137
for (let i = 0; i < loadedAppNames.length; i++) {
optionElements.push(<VSCodeOption>{loadedAppNames[i]}</VSCodeOption>);
if (loadedAppNames[i] == appName) {
seenHistoryAppName = true;
}
}
Copy link
Contributor

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}
                />
              ))}

from https://github.com/saucelabs/vscode-scriptiq/blob/main/webview-ui/test-generation/src/App.tsx#L373-L380

@@ -68,6 +68,10 @@
row-gap: 8px;
}

.app-not-loaded {
color: grey;
Copy link
Contributor

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.

Comment on lines 166 to 172
className={
optionElements.some(
(option) => option.props.className === 'app-not-loaded',
)
? 'app-not-loaded'
: ''
}
Copy link
Contributor

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:

Suggested change
className={
optionElements.some(
(option) => option.props.className === 'app-not-loaded',
)
? 'app-not-loaded'
: ''
}
className="app-list"

Copy link
Contributor Author

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

Copy link
Contributor

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...

Comment on lines 92 to 94
const data = (await response.json()) as { items: AppInfo[] };

if (data && Array.isArray(data.items) && data.items.every(isAppInfo)) {
Copy link
Contributor

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.

);

if (!response.ok) {
throw new Error('Network response was not ok');
Copy link
Contributor

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.

Comment on lines 98 to 99
TestGenerationPanel.currentPanel.loadAppNames();

Copy link
Contributor

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?

Copy link
Contributor Author

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';
Copy link
Contributor

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...

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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[];
Copy link
Contributor

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?

);

if (!response.ok) {
throw new Error('Network response was not ok');

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.',
as well, where you have to make a guess about what could be wrong. We'd definitely know if we get an unauthorized response back. I wouldn't hide the error, just show it to the user.

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[] = [];

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!

Copy link
Contributor Author

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

Copy link

@alexplischke alexplischke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some selections don't appear to be working 🤔
SCR-20240911-mpvy

@@ -72,3 +72,28 @@ export async function sendUserRating(
throw new Error('Unexpected status code: ' + resp.status);
}
}

export async function fetchAppNames(creds: Credentials) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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 🙏
  2. Won't this return both VDC and RDC type apps? Are we fully supporting VDC now?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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',
Copy link
Contributor

@mhan83 mhan83 Sep 13, 2024

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

Copy link
Contributor Author

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.

Comment on lines 95 to 99
return data.items.filter(
(item) =>
!('is_simulator' in item.metadata) ||
item.metadata.is_simulator === false,
);
Copy link
Contributor

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>;
Copy link
Contributor

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:

Suggested change
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:

Suggested change
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.

item.metadata.is_simulator === false,
);
} else {
throw new Error('Unexpected data format from API');

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 🔍

Comment on lines 183 to 185
toast.showError(
'Failed to load app names. Please check your credentials and try again.',
);

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.

Suggested change
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';

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',
for instance, forcing you to potentially lie to the compiler.

Food for thought:

  1. 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.
  2. 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.
  3. 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.

Copy link
Contributor Author

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.

Copy link
Contributor

@mhan83 mhan83 left a 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 ||

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?

Copy link
Contributor Author

@lenareed lenareed Sep 16, 2024

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.

Copy link
Contributor

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.

Comment on lines 373 to 374
!('is_simulator' in item.metadata) ||
item.metadata.is_simulator === false,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified:

Suggested change
!('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';

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',

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';
, this line will be complicit in the deception of the compiler.

Options

  1. 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 type Platform.
  2. 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,

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;
is already doing a cast.

@alexplischke
Copy link

I believe that the title can be renamed from feat!: drop down of apps from app management to feat: drop down of apps from app management, since this implementation is fully backwards compatible now.

@lenareed lenareed changed the title feat!: drop down of apps from app management feat: drop down of apps from app management Sep 16, 2024
@lenareed
Copy link
Contributor Author

@mhan83 for the question about the error message, you get a device connection failed message very quickly, it looks like this
Device Connection Failed: Message: Unable to find file 'petals.apk' in app storage Build info: version: 'unknown', revision: 'unknown', time: 'unknown' System info: host: 'Sauce Labs RDC', ip: 'N/A', os.name: 'Linux', os.arch: 'amd64', os.version: '6.1.85+', java.version: '17.0.12' Driver info: driver.version: unknown
I think that is an informative error, though we could make the format nicer if we want

@lenareed lenareed merged commit 2e6d327 into main Sep 17, 2024
1 check passed
@lenareed lenareed deleted the DEVX-2886 branch September 17, 2024 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants