-
Notifications
You must be signed in to change notification settings - Fork 5
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: richer empty states #239
Conversation
Minder Vulnerability Report ✅Minder analyzed this PR and found it does not add any new vulnerable dependencies.
|
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.
Dependency Information
Minder analyzed the dependencies introduced in this pull request and detected that some dependencies do not meet your security profile.
📦 Dependency: ts-pattern
Trusty Score: 0
Scoring details
Component | Score |
---|---|
Package activity | 7.3 |
Repository activity | 5.7 |
User activity | 8.9 |
Provenance | historical_provenance_match |
Proof of Origin (Provenance)
This package can be linked back to its source code using a historical provenance map.
We were able to correlate a significant number of git tags and tagged releases in this package’s source code to versions of the published package. This mapping creates a strong link from the package back to its source code repository, verifying proof of origin.
Published package versions | 155 |
Number of git tags or releases | 63 |
Versions matched to tags or releases | 55 |
Pull Request Test Coverage Report for Build 13109301751Details
💛 - Coveralls |
if (testCase.expected.actions) { | ||
for (const action of testCase.expected.actions) { | ||
const actionButton = getByRole(action.role, { name: action.name }); | ||
expect(actionButton).toBeVisible(); | ||
if (action.href) { | ||
expect(actionButton).toHaveAttribute("href", action.href); | ||
} | ||
} | ||
} |
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.
Breaking the rules of testing here a bit, but this module would be a lot longer otherwise.
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 you are referring to the if
statement I think it's fine, but it looks like the if statement is actually kinda redundant, no?
I mean iterating over 0 items without condition has the same effect as not iterating over 0 items based on a condition 🤔
for (const action of testCase.expected.actions ?? []) {
const actionButton = getByRole(action.role, { name: action.name });
expect(actionButton).toBeVisible();
if (action.href) {
expect(actionButton).toHaveAttribute("href", action.href);
}
}
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.
on the other hand, I think there is a slightly different way to organize the code that does not even require the for loop 🤔
you could make a function like this:
function expectAction({role = "button", name, href}) {
const actionElement = screen.getByRole(role, { name })
expect(actionElement).toBeVisible()
expect(actionElement).toHaveAttribute("href", href)
}
Then you could pass in the action assertions as an arrow function:
expected: {
title: emptyStateStrings.title.noAlertsFoundWorkspace,
body: emptyStateStrings.body.alertsWillShowUpWhenWorkspace,
illustrationTestId: IllustrationTestId.DONE,
expectActions: () => {
expectAction({role: "link", name: /manage workspaces/i, href: etc})
}
},
or you can even be fancy and make a custom assertion that can be used like this
expected: {
title: emptyStateStrings.title.noAlertsFoundWorkspace,
body: emptyStateStrings.body.alertsWillShowUpWhenWorkspace,
illustrationTestId: IllustrationTestId.DONE,
expectActions: () => {
expect.hrefElement("link", { name: /manage workspaces/i, href: etc})
}
},
@@ -0,0 +1,25 @@ | |||
export const emptyStateStrings = { |
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.
do we need to extract strings like this? cc. @peppescg
since it's not used for translations I don't see the added value. I think in 8 out of 10 cases putting a static text in the component directly results in more navigable code, since you don't need 2 searches to find where the text is used 🤔
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.
also having the text directly in the component helps the readability of the code, makes it more intuitive to connect what you see in the browser with what you see in the editor
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 find it useful for testing, you can just reference the same variable and it's requires less changes if you update the copy.
Also — not relevant to this implementation — but to me it is good for making the structure of jsx/html easier to parse. Although here I'm just returning it from within a function.
I actually usually use a module with top level exports, which you can scan with dead code analysis tools too.
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 interesting. I see the usefulness of that, albeit I prefer that user-facing changes actually require changes to test code, as I try to see the test code as a "representative" of the user in our codebase.
I sometimes use the approach of only matching the relevant keywords when repeating long text gets annoying:
expect(screen.getByText(/.*no.*alerts.*/i).toBeVisible()
That is fine while working with the assumptions that tests are a simulation of user behavior, because I think the user's eyes are also automatically looking for the important keywords rather than carefully reading the text letter-by-letter.
It might be still slightly more annoying that your approach - but this could be a feature, not a bug, perhaps it helps us understand that whenever we get this annoyance, an experienced user might get the same annoyance finding that the behavior or wording changed 🤔
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 dunno, I think wildcard regex in tests will likely be a foot gun eventually due to fiddly nature of greedy vs lazy matching.
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 dunno, I think wildcard regex in tests will likely be a foot gun eventually due to fiddly nature of greedy vs lazy matching.
it can be, but mostly if the keywords are not actually good keywords, or if the same keywords end up being used a lot more in the same page. But this would actually be a signal that they keywords no longer cognitively help the user as much as well 🤔 which might be a sign that the route/page/view is becoming overly complex
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 agree with @kantord, also if we want to do this we should do it everywhere, not only for a single component. IMO the application is too small so I guess for now thinking a strategy for text is early.
Cause we are in fast development mode, we can merge it, avoiding wasting time today...but I would avoid to use this approach until we don't need to think a text/translation issue
search: P._, | ||
view: P._, | ||
}, | ||
() => ({ |
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 can use pattern matching here, but:
- I would still leverage early-return pattern as much as possible in order to simplify the pattern matching instead of merging everything into a single pattern. For instance if I understand correctly, when
isLoading = true
, then none of the other values in the pattern have any relevance. - This function is very long, and has some heavy parts that are not comprehensible at first glance. I would improve that by breaking it down into multiple functions/hooks, which would allow you to replace complex blocks of code with names.
Like so:
.with(
{
hasWorkspaceAlerts: true,
hasMultipleWorkspaces: P.any,
search: P.string.select(),
view: P._,
},
() => useAlertsEmptyStateNoSearchResults()
)
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.
also, I'm not sure I understand why this is a hook instead of a component 🤔 I would actually expect sth like this
.with(
{
hasWorkspaceAlerts: true,
hasMultipleWorkspaces: P.any,
search: P.string.select(),
view: P._,
},
<AlertsEmptyStateNoSearchResults />
)
/* ...etc... */
function AlertsEmptyStateNoSearchResults() {
return (<GenericTableEmptyState
title="No matching alerts found"
illustration={<FooBarBaz />}
actions={<Button onPress={etc}>Clear search</Button> />)
}
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 a hook so it can call other hooks, which it does to check the length of alerts/workspaces, and to get the query params used for filtering.
Re: early return — the first arm of the match statement is effectively an early return on isMatching: true
— doesn't this achieve the same thing?
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 do see some merit in using a component rather than a struct for the content
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 not sure I understand why this is a hook instead of a component 🤔
I though more about your comment — I initially wrote this as a component, but decided to separate the logic into this hook, as was quite verbose.
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 guess I am not extremely against using the hook for the logic of choosing between different values.
I hold this part of the opinion stronger: https://github.com/stacklok/codegate-ui/pull/239/files#r1937429121
about not directly binding the very specific logic with the very generic one.
); | ||
} | ||
|
||
export function TableAlertsEmptyState({ isLoading }: { isLoading: boolean }) { |
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.
look at this suggestion: https://github.com/stacklok/codegate-ui/pull/239/files#r1937422209
I think that this component could be a completely generic one, and probably should in order to encourage the reuse of the same pattern 🤔
It feels weird to bind this very generic representation logic (with all actual content being parametric) with a very specific logic in the useAlertsEmptyState
hook. Even if you prefer the hook method, I would not bind it to this component, it could be called in a more alerts table-specific context
import { TableAlerts } from "../table-alerts"; | ||
import { hrefs } from "@/lib/hrefs"; | ||
|
||
enum IllustrationTestId { |
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 am on the fence about whether we even need to test a purely visual illustration that has little functional purpose. If it had an actual functional purpose, it would not need test IDs but aria-labels/alt texts, no? 🤔
I just have this feeling that these illustrations don't actually illustrate anything and are only eye candy, so it would only need to be tested using visual regression tests
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 say that but I had the wrong illustration in one of the match arms :)
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.
hahaaha, I don't doubt that, I mean, if we had a good, easy-to-use, reliable, fast and cheap visual regression testing method, then I would recommend that we write a visual regression test for each case
</div> | ||
); | ||
default: | ||
return column.id satisfies never; |
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.
why not
return column.id satisfies never; | |
throw new Error('tried to render non existent column') |
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.
or use ts-pattern with .exhaustive()
to get type safety guarantees that it does not happen
</div> | ||
); | ||
} | ||
switch (column.id) { |
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 am a bit confused about this refactor, seems like it adds a lot of logic but I don't really get the benefit? 🤔 since in the end it just reimplements the hardcoded code in a more complicated way? 🤔
In a simplified way, this is what my eyes see here:
Original code
<X>
<Foo />
<Bar />
</X>
Refactored code:
<X columns=[Foo, Bar]>
(item) => child(item)
</X>
/* ... etc ... */
function child(item) {
if (item == Foo) {
return <Foo />
}
if (item == Bar) {
return <Bar />
}
}
So I just see some added logic that does not change behavior 🤔 what am I missing?
in any case, if we really need this function and switch-case logic, why not use ts-pattern here as well?
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 actually re-wrote it while chasing a bug, that lay elsewhere. I will separate it into it's own PR.
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 going to leave this in — mainly because James requested some styling tweaks to the cells in the table component, which are done, and they're easiest applied when using collections to render the cells.
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.
Dependency Information
Minder analyzed the dependencies introduced in this pull request and detected that some dependencies do not meet your security profile.
📦 Dependency: ts-pattern
Trusty Score: 0
Scoring details
Component | Score |
---|---|
Package activity | 7.3 |
Repository activity | 5.7 |
User activity | 8.9 |
Provenance | historical_provenance_match |
Proof of Origin (Provenance)
This package can be linked back to its source code using a historical provenance map.
We were able to correlate a significant number of git tags and tagged releases in this package’s source code to versions of the published package. This mapping creates a strong link from the package back to its source code repository, verifying proof of origin.
Published package versions | 155 |
Number of git tags or releases | 63 |
Versions matched to tags or releases | 55 |
ts-pattern
to do the pattern matching across a range of scenariosScreenshots
This is not an exhaustive list