Skip to content

Commit

Permalink
web-wallet: Add ExclusiveChoice component
Browse files Browse the repository at this point in the history
Resolves #2070
  • Loading branch information
ascartabelli committed Aug 6, 2024
1 parent fa4ebf1 commit 9523128
Show file tree
Hide file tree
Showing 13 changed files with 681 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.dusk-exclusive-choice {
display: inline-flex;
align-items: center;
justify-content: center;
}

.dusk-exclusive-choice__label {
flex: 1;
}

.dusk-exclusive-choice__radio {
position: absolute;
top: -9999px;
left: -9999px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<svelte:options immutable={true} />

<script>
import { isType } from "lamb";
import { makeClassName, randomUUID } from "$lib/dusk/string";
import "./ExclusiveChoice.css";
/** @type {string | undefined} */
export let className = undefined;
/** @type {string | undefined} */
export let name = undefined;
/** @type {SelectOption[] | String[]} */
export let options;
/** @type {string} */
export let value;
/** @type {(v: any) => v is string} */
const isString = isType("String");
const baseId = randomUUID();
$: classes = makeClassName(["dusk-exclusive-choice", className]);
</script>

<div class={classes} role="radiogroup" {...$$restProps}>
{#each options as option (option)}
{@const isStringOption = isString(option)}
{@const optionValue = isStringOption ? option : option.value}
{@const id = `${baseId}-${optionValue}`}
<input
bind:group={value}
class="dusk-exclusive-choice__radio"
checked={optionValue === value}
disabled={isStringOption ? false : option.disabled}
{id}
name={name ?? baseId}
on:change
type="radio"
value={optionValue}
/>
<label class="dusk-exclusive-choice__label" for={id}
>{isStringOption ? option : option.label ?? optionValue}</label
>
{/each}
</div>
112 changes: 112 additions & 0 deletions web-wallet/src/lib/dusk/components/__tests__/ExclusiveChoice.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render } from "@testing-library/svelte";

import { ExclusiveChoice } from "..";

vi.mock("$lib/dusk/string", async (importOriginal) => {
/** @type {typeof import("$lib/dusk/string")} */
const original = await importOriginal();

return {
...original,
randomUUID: () => "some-generated-id",
};
});

describe("ExclusiveChoice", () => {
const stringOptions = ["one", "two", "three", "four"];

/** @type {SelectOption[]} */
const objectOptionsA = [
{ label: "one", value: "1" },
{ label: "two", value: "2" },
{ disabled: true, label: "three", value: "3" },
{ label: "four", value: "4" },
];

/** @type {SelectOption[]} */
const objectOptionsB = [
{ value: "1" },
{ value: "2" },
{ value: "3" },
{ value: "4" },
];

const baseProps = {
options: objectOptionsA,
value: "2",
};
const baseOptions = {
props: baseProps,
target: document.body,
};

afterEach(cleanup);

afterAll(() => {
vi.doUnmock("$lib/dusk/string");
});

it("should render the `ExclusiveChoice` component", () => {
const { container } = render(ExclusiveChoice, baseOptions);

expect(container.firstChild).toMatchSnapshot();
});

it("should accept a custom name for the radio elements", () => {
const props = {
...baseProps,
name: "my-custom-name",
};
const { container } = render(ExclusiveChoice, { ...baseOptions, props });

expect(container.firstChild).toMatchSnapshot();
});

it("should accept an array of options object without labels and use the value as labels", () => {
const props = {
...baseProps,
options: objectOptionsB,
};
const { container } = render(ExclusiveChoice, { ...baseOptions, props });

expect(container.firstChild).toMatchSnapshot();
});

it("should accept an array of string as options", () => {
const props = {
...baseProps,
options: stringOptions,
};
const { container } = render(ExclusiveChoice, { ...baseOptions, props });

expect(container.firstChild).toMatchSnapshot();
});

it("should pass additional class names and attributes to the rendered element", () => {
const props = {
...baseProps,
className: "foo bar",
id: "some-id",
};
const { container } = render(ExclusiveChoice, { ...baseOptions, props });

expect(container.firstChild).toMatchSnapshot();
});

it("should accept a change event handler", async () => {
const changeHandler = vi.fn();
const { component, container } = render(ExclusiveChoice, baseOptions);
const target = /** @type {HTMLInputElement} */ (
container.querySelector("input[value='4']")
);

component.$on("change", changeHandler);

await fireEvent.click(target);

expect(changeHandler).toHaveBeenCalledTimes(1);
expect(changeHandler).toHaveBeenCalledWith(expect.any(Event));
expect(target.checked).toBe(true);
});
});
Loading

0 comments on commit 9523128

Please sign in to comment.