mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
refactor: show icons for multi-select parameter options (#18594)
This commit is contained in:
@@ -259,18 +259,16 @@ We use [Formik](https://formik.org/docs) for forms along with
|
||||
|
||||
## Testing
|
||||
|
||||
We use three types of testing in our app: **End-to-end (E2E)**, **Integration**
|
||||
We use three types of testing in our app: **End-to-end (E2E)**, **Integration/Unit**
|
||||
and **Visual Testing**.
|
||||
|
||||
### End-to-End (E2E)
|
||||
### End-to-End (E2E) – Playwright
|
||||
|
||||
These are useful for testing complete flows like "Create a user", "Import
|
||||
template", etc. We use [Playwright](https://playwright.dev/). If you only need
|
||||
to test if the page is being rendered correctly, you should consider using the
|
||||
**Visual Testing** approach.
|
||||
template", etc. We use [Playwright](https://playwright.dev/). These tests run against a full Coder instance, backed by a database, and allows you to make sure that features work properly all the way through the stack. "End to end", so to speak.
|
||||
|
||||
For scenarios where you need to be authenticated, you can use
|
||||
`test.use({ storageState: getStatePath("authState") })`.
|
||||
For scenarios where you need to be authenticated as a certain user, you can use
|
||||
`login` helper. Passing it some user credentials will log out of any other user account, and will attempt to login using those credentials.
|
||||
|
||||
For ease of debugging, it's possible to run a Playwright test in headful mode
|
||||
running a Playwright server on your local machine, and executing the test inside
|
||||
@@ -289,22 +287,14 @@ local machine and forward the necessary ports to your workspace. At the end of
|
||||
the script, you will land _inside_ your workspace with environment variables set
|
||||
so you can simply execute the test (`pnpm run playwright:test`).
|
||||
|
||||
### Integration
|
||||
### Integration/Unit – Jest
|
||||
|
||||
Test user interactions like "Click in a button shows a dialog", "Submit the form
|
||||
sends the correct data", etc. For this, we use [Jest](https://jestjs.io/) and
|
||||
[react-testing-library](https://testing-library.com/docs/react-testing-library/intro/).
|
||||
If the test involves routing checks like redirects or maybe checking the info on
|
||||
another page, you should probably consider using the **E2E** approach.
|
||||
We use Jest mostly for testing code that does _not_ pertain to React. Functions and classes that contain notable app logic, and which are well abstracted from React should have accompanying tests. If the logic is tightly coupled to a React component, a Storybook test or an E2E test may be a better option depending on the scenario.
|
||||
|
||||
### Visual testing
|
||||
### Visual Testing – Storybook
|
||||
|
||||
We use visual tests to test components without user interaction like testing if
|
||||
a page/component is rendered correctly depending on some parameters, if a button
|
||||
is showing a spinner, if `loading` props are passed correctly, etc. This should
|
||||
always be your first option since it is way easier to maintain. For this, we use
|
||||
[Storybook](https://storybook.js.org/) and
|
||||
[Chromatic](https://www.chromatic.com/).
|
||||
We use Storybook for testing all of our React code. For static components, you simply add a story that renders the components with the props that you would like to test, and Storybook will record snapshots of it to ensure that it isn't changed unintentionally. If you would like to test an interaction with the component, then you can add an interaction test by specifying a `play` function for the story. For stories with an interaction test, a snapshot will be recorded of the end state of the component. We use
|
||||
[Chromatic](https://www.chromatic.com/) to manage and compare snapshots in CI.
|
||||
|
||||
To learn more about testing components that fetch API data, refer to the
|
||||
[**Where to fetch data**](#where-to-fetch-data) section.
|
||||
|
||||
+89
-4
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 11 KiB |
@@ -39,6 +39,23 @@ export const OpenCombobox: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
args: {
|
||||
options: organizations.map((org) => ({
|
||||
label: org.display_name,
|
||||
value: org.id,
|
||||
icon: org.icon,
|
||||
})),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByPlaceholderText("Select organization"));
|
||||
await waitFor(() =>
|
||||
expect(canvas.getByText("My Organization")).toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectComboboxItem: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -98,3 +115,49 @@ export const ClearAllComboboxItems: Story = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithGroups: Story = {
|
||||
args: {
|
||||
placeholder: "Make a playlist",
|
||||
groupBy: "album",
|
||||
options: [
|
||||
{
|
||||
label: "Photo Facing Water",
|
||||
value: "photo-facing-water",
|
||||
album: "Papillon",
|
||||
icon: "/emojis/1f301.png",
|
||||
},
|
||||
{
|
||||
label: "Mercurial",
|
||||
value: "mercurial",
|
||||
album: "Papillon",
|
||||
icon: "/emojis/1fa90.png",
|
||||
},
|
||||
{
|
||||
label: "Merging",
|
||||
value: "merging",
|
||||
album: "Papillon",
|
||||
icon: "/lol-not-a-real-image.png",
|
||||
},
|
||||
{
|
||||
label: "Flacks",
|
||||
value: "flacks",
|
||||
album: "aBliss",
|
||||
// intentionally omitted icon
|
||||
},
|
||||
{
|
||||
label: "aBliss",
|
||||
value: "abliss",
|
||||
album: "aBliss",
|
||||
// intentionally omitted icon
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByPlaceholderText("Make a playlist"));
|
||||
await waitFor(() =>
|
||||
expect(canvas.getByText("Papillon")).toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @see {@link https://shadcnui-expansions.typeart.cc/docs/multiple-selector}
|
||||
*/
|
||||
import { Command as CommandPrimitive, useCommandState } from "cmdk";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import {
|
||||
Command,
|
||||
@@ -30,6 +31,7 @@ import { cn } from "utils/cn";
|
||||
export interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
disable?: boolean;
|
||||
/** fixed option that can't be removed. */
|
||||
fixed?: boolean;
|
||||
@@ -353,7 +355,9 @@ export const MultiSelectCombobox = forwardRef<
|
||||
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus, onSearch]);
|
||||
|
||||
const CreatableItem = () => {
|
||||
if (!creatable) return undefined;
|
||||
if (!creatable) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
isOptionsExist(options, [{ value: inputValue, label: inputValue }]) ||
|
||||
selected.find((s) => s.value === inputValue)
|
||||
@@ -437,6 +441,7 @@ export const MultiSelectCombobox = forwardRef<
|
||||
}
|
||||
|
||||
const fixedOptions = selected.filter((s) => s.fixed);
|
||||
const showIcons = arrayOptions?.some((it) => it.icon);
|
||||
|
||||
return (
|
||||
<Command
|
||||
@@ -487,7 +492,16 @@ export const MultiSelectCombobox = forwardRef<
|
||||
data-fixed={option.fixed}
|
||||
data-disabled={disabled || undefined}
|
||||
>
|
||||
{option.label}
|
||||
<div className="flex items-center gap-1">
|
||||
{option.icon && (
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={option.icon}
|
||||
fallback={option.label}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="clear-option-button"
|
||||
@@ -639,7 +653,16 @@ export const MultiSelectCombobox = forwardRef<
|
||||
"cursor-default text-content-disabled",
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
<div className="flex items-center gap-2">
|
||||
{showIcons && (
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={option.icon}
|
||||
fallback={option.label}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -433,6 +433,7 @@ const ParameterField: FC<ParameterFieldProps> = ({
|
||||
const options: Option[] = parameter.options.map((opt) => ({
|
||||
value: opt.value.value,
|
||||
label: opt.name,
|
||||
icon: opt.icon,
|
||||
disable: false,
|
||||
}));
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"jfrog.svg",
|
||||
"jupyter.svg",
|
||||
"k8s.png",
|
||||
"k8s.svg",
|
||||
"kasmvnc.svg",
|
||||
"keycloak.svg",
|
||||
"kotlin.svg",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user