@smicolon/fasla-ui 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -20
- package/dist/index.cjs +319 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +92 -24
- package/dist/lib/utils.cjs +11 -0
- package/dist/lib/utils.d.cts +9 -0
- package/dist/motion/index.cjs +61 -0
- package/dist/motion/index.d.cts +33 -0
- package/dist/registry/index.cjs +179 -0
- package/dist/registry/index.d.cts +17 -0
- package/dist/registry/index.js +32 -3
- package/dist/tokens/index.cjs +77 -0
- package/dist/tokens/index.d.cts +124 -0
- package/dist/tokens/index.d.ts +75 -6
- package/dist/tokens/index.js +59 -20
- package/package.json +17 -7
- package/registry/blocks/app-shell/AppShell.tsx +1 -1
- package/registry/blocks/data-table/DataTable.test.tsx +61 -0
- package/registry/blocks/data-table/DataTable.tsx +53 -16
- package/registry/blocks/data-table/index.ts +1 -0
- package/registry/blocks/empty-state/EmptyState.test.tsx +23 -0
- package/registry/blocks/empty-state/EmptyState.tsx +32 -33
- package/registry/blocks/form-section/FormSection.tsx +1 -1
- package/registry/blocks/navbar/Navbar.test.tsx +12 -0
- package/registry/blocks/navbar/Navbar.tsx +8 -23
- package/registry/blocks/page-header/PageHeader.tsx +5 -2
- package/registry/blocks/sidebar/Sidebar.test.tsx +39 -0
- package/registry/blocks/sidebar/Sidebar.tsx +36 -26
- package/registry/blocks/stats-card/StatsCard.tsx +12 -29
- package/registry/effects/animated-gradient/animated-gradient.tsx +19 -8
- package/registry/effects/border-beam/border-beam.tsx +77 -53
- package/registry/effects/effects.test.tsx +266 -0
- package/registry/effects/glow-card/glow-card.tsx +68 -43
- package/registry/effects/shimmer-button/shimmer-button.tsx +62 -23
- package/registry/effects/spotlight/spotlight.tsx +73 -29
- package/registry/effects/text-reveal/index.ts +1 -0
- package/registry/effects/text-reveal/text-reveal.test.tsx +66 -0
- package/registry/effects/text-reveal/text-reveal.tsx +70 -21
- package/registry/effects/typewriter-text/typewriter-text.tsx +39 -20
- package/registry/next14-tsconfig.test.ts +43 -0
- package/registry/text-props-undefined.test.tsx +73 -0
- package/registry/ui/avatar/avatar.test.tsx +316 -0
- package/registry/ui/avatar/avatar.tsx +235 -108
- package/registry/ui/avatar/index.ts +1 -1
- package/registry/ui/badge/badge.test.tsx +267 -32
- package/registry/ui/badge/badge.tsx +233 -21
- package/registry/ui/button/button.test.tsx +6 -0
- package/registry/ui/button/button.tsx +6 -23
- package/registry/ui/card/card.tsx +1 -1
- package/registry/ui/checkbox/checkbox.test.tsx +23 -0
- package/registry/ui/checkbox/checkbox.tsx +16 -27
- package/registry/ui/combobox/combobox.test.tsx +72 -0
- package/registry/ui/combobox/combobox.tsx +34 -59
- package/registry/ui/input/input.test.tsx +3 -3
- package/registry/ui/input/input.tsx +5 -5
- package/registry/ui/radio/index.ts +1 -0
- package/registry/ui/radio/radio.test.tsx +248 -0
- package/registry/ui/radio/radio.tsx +208 -0
- package/registry/ui/select/select.tsx +4 -14
- package/registry/ui/skeleton/index.ts +1 -0
- package/registry/ui/skeleton/skeleton.test.tsx +113 -29
- package/registry/ui/skeleton/skeleton.tsx +101 -41
- package/registry/ui/status-indicator/index.ts +6 -0
- package/registry/ui/status-indicator/status-indicator.test.tsx +148 -0
- package/registry/ui/status-indicator/status-indicator.tsx +87 -0
- package/registry/ui/switch/index.ts +1 -1
- package/registry/ui/switch/switch.test.tsx +323 -0
- package/registry/ui/switch/switch.tsx +254 -57
- package/registry/ui/textarea/textarea.tsx +1 -1
- package/registry.json +79 -16
- package/tailwind-preset.ts +143 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { dirname, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { describe, it, expect } from "vitest"
|
|
6
|
+
import { render, screen, fireEvent } from "@testing-library/react"
|
|
7
|
+
import { Avatar, type AvatarProps } from "./avatar"
|
|
8
|
+
|
|
9
|
+
const SOURCE = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "avatar.tsx"), "utf8")
|
|
10
|
+
|
|
11
|
+
function avatar(props: Partial<AvatarProps> = {}) {
|
|
12
|
+
const { container } = render(<Avatar data-testid="avatar" {...props} />)
|
|
13
|
+
// Scoped to this render: some tests draw several avatars.
|
|
14
|
+
const root = container.querySelector<HTMLElement>("[data-testid='avatar']")!
|
|
15
|
+
const frame = root.firstElementChild as HTMLElement
|
|
16
|
+
return { root, frame, container }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("Avatar", () => {
|
|
20
|
+
it("renders Figma's defaults: 32, Standard, no border, no dot", () => {
|
|
21
|
+
const { root, frame } = avatar({ name: "Layla Hassan" })
|
|
22
|
+
expect(root).toHaveClass("size-8")
|
|
23
|
+
expect(frame).toHaveClass("rounded-[var(--radius-md)]", "bg-muted", "text-foreground", "text-base", "overflow-hidden")
|
|
24
|
+
expect(frame.className).not.toMatch(/after:ring/)
|
|
25
|
+
expect(root.querySelector(".rounded-full.bg-success")).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("size × radius", () => {
|
|
29
|
+
it.each([
|
|
30
|
+
["32", "size-8", "rounded-[var(--radius-md)]", "text-base"],
|
|
31
|
+
["24", "size-6", "rounded-[var(--radius-md)]", "text-xs"],
|
|
32
|
+
// Figma's XXS: 10px, leaded 14 in English and 16 in Arabic.
|
|
33
|
+
["12", "size-3", "rounded-[var(--radius-xs)]", "text-xxs"],
|
|
34
|
+
] as const)("Standard %s is %s, %s, %s", (size, box, corner, text) => {
|
|
35
|
+
const { root, frame } = avatar({ size, name: "Layla" })
|
|
36
|
+
expect(root).toHaveClass(box)
|
|
37
|
+
expect(frame).toHaveClass(corner, text)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it.each(["32", "24", "12"] as const)("Rounded %s is a circle", (size) => {
|
|
41
|
+
const { frame } = avatar({ size, radius: "rounded" })
|
|
42
|
+
expect(frame).toHaveClass("rounded-full")
|
|
43
|
+
expect(frame.className).not.toMatch(/radius-(md|xs)/)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("border", () => {
|
|
48
|
+
it.each([
|
|
49
|
+
["32", "after:ring-2"],
|
|
50
|
+
["24", "after:ring-1"],
|
|
51
|
+
["12", "after:ring-1"],
|
|
52
|
+
] as const)("draws a `ring` stroke inside the edge at %s: %s", (size, width) => {
|
|
53
|
+
const { frame } = avatar({ size, border: true })
|
|
54
|
+
expect(frame).toHaveClass("after:ring-inset", "after:ring-ring", "after:absolute", "after:inset-0", width)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("variant=image (the default) falls back through the content", () => {
|
|
59
|
+
it("shows the photo, named by `name`, once it loads", () => {
|
|
60
|
+
const { frame } = avatar({ src: "/layla.png", name: "Layla Hassan" })
|
|
61
|
+
const img = frame.querySelector("img")!
|
|
62
|
+
// Still loading: hidden and unnamed, while the named fallback shows.
|
|
63
|
+
expect(img).toHaveClass("opacity-0")
|
|
64
|
+
expect(img).toHaveAttribute("aria-hidden", "true")
|
|
65
|
+
expect(frame).toHaveClass("bg-muted")
|
|
66
|
+
expect(frame.querySelector(".sr-only")!.textContent).toBe("Layla Hassan")
|
|
67
|
+
fireEvent.load(img)
|
|
68
|
+
expect(screen.getByRole("img", { name: "Layla Hassan" })).toBe(img) // same element, no re-decode
|
|
69
|
+
expect(img).not.toHaveClass("opacity-0")
|
|
70
|
+
expect(frame).not.toHaveClass("bg-muted") // Figma's Image variant has no fill
|
|
71
|
+
expect(frame.textContent).not.toContain("LH")
|
|
72
|
+
expect(frame.querySelector(".sr-only")).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("never shows the previous photo under a new src: the fallback shows until it loads", () => {
|
|
76
|
+
const { container, rerender } = render(<Avatar src="/a.png" name="Layla Hassan" />)
|
|
77
|
+
fireEvent.load(container.querySelector("img")!)
|
|
78
|
+
rerender(<Avatar src="/b.png" name="Vera Brandt" />)
|
|
79
|
+
const imgs = container.querySelectorAll("img")
|
|
80
|
+
expect(imgs).toHaveLength(1)
|
|
81
|
+
expect(imgs[0]).toHaveAttribute("src", "/b.png") // the old photo is gone at once
|
|
82
|
+
expect(imgs[0]).toHaveClass("opacity-0")
|
|
83
|
+
expect(screen.queryAllByRole("img")).toHaveLength(0) // nothing named by the old photo
|
|
84
|
+
expect(container.textContent).toContain("VB")
|
|
85
|
+
expect(container.querySelector(".sr-only")!.textContent).toBe("Vera Brandt")
|
|
86
|
+
fireEvent.load(imgs[0]!)
|
|
87
|
+
expect(screen.getByRole("img", { name: "Vera Brandt" })).toHaveAttribute("src", "/b.png")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("resets on a new srcSet too", () => {
|
|
91
|
+
const { container, rerender } = render(<Avatar src="/a.png" srcSet="/a.png 1x" name="Layla Hassan" />)
|
|
92
|
+
fireEvent.load(container.querySelector("img")!)
|
|
93
|
+
rerender(<Avatar src="/a.png" srcSet="/a@2x.png 2x" name="Layla Hassan" />)
|
|
94
|
+
expect(container.querySelector("img")).toHaveClass("opacity-0")
|
|
95
|
+
expect(container.textContent).toContain("LH")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("tries a source that failed before afresh when it comes back", () => {
|
|
99
|
+
const { container, rerender } = render(<Avatar src="/flaky.png" name="Layla Hassan" />)
|
|
100
|
+
fireEvent.error(container.querySelector("img")!)
|
|
101
|
+
expect(container.querySelector("img")).toBeNull()
|
|
102
|
+
rerender(<Avatar src="/other.png" name="Layla Hassan" />)
|
|
103
|
+
rerender(<Avatar src="/flaky.png" name="Layla Hassan" />)
|
|
104
|
+
const retry = container.querySelector("img")!
|
|
105
|
+
expect(retry).toHaveAttribute("src", "/flaky.png") // requested again, not remembered as failed
|
|
106
|
+
fireEvent.load(retry)
|
|
107
|
+
expect(screen.getByRole("img", { name: "Layla Hassan" })).toBe(retry)
|
|
108
|
+
expect(container.textContent).not.toContain("LH")
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe("a photo that finished before its listeners were attached", () => {
|
|
112
|
+
// As after hydration, or on a reload served from cache: the browser has
|
|
113
|
+
// already loaded the image, so no load or error event will ever arrive.
|
|
114
|
+
const stub = (complete: boolean, naturalWidth: number) => {
|
|
115
|
+
const proto = HTMLImageElement.prototype
|
|
116
|
+
const was = {
|
|
117
|
+
complete: Object.getOwnPropertyDescriptor(proto, "complete")!,
|
|
118
|
+
naturalWidth: Object.getOwnPropertyDescriptor(proto, "naturalWidth")!,
|
|
119
|
+
}
|
|
120
|
+
Object.defineProperty(proto, "complete", { configurable: true, get: () => complete })
|
|
121
|
+
Object.defineProperty(proto, "naturalWidth", { configurable: true, get: () => naturalWidth })
|
|
122
|
+
return () => {
|
|
123
|
+
Object.defineProperty(proto, "complete", was.complete)
|
|
124
|
+
Object.defineProperty(proto, "naturalWidth", was.naturalWidth)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
it("shows it at once when it is complete with a real size", () => {
|
|
129
|
+
const restore = stub(true, 64)
|
|
130
|
+
try {
|
|
131
|
+
const { frame } = avatar({ src: "/cached.png", name: "Layla Hassan" })
|
|
132
|
+
const img = screen.getByRole("img", { name: "Layla Hassan" }) // no load event fired
|
|
133
|
+
expect(img).not.toHaveClass("opacity-0")
|
|
134
|
+
expect(frame).not.toHaveClass("bg-muted")
|
|
135
|
+
expect(frame.textContent).not.toContain("LH")
|
|
136
|
+
} finally {
|
|
137
|
+
restore()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("falls back when it is complete but empty — it failed before we listened", () => {
|
|
142
|
+
const restore = stub(true, 0)
|
|
143
|
+
try {
|
|
144
|
+
const { frame } = avatar({ src: "/broken.png", name: "Layla Hassan" })
|
|
145
|
+
expect(frame.querySelector("img")).toBeNull()
|
|
146
|
+
expect(frame.textContent).toContain("LH")
|
|
147
|
+
} finally {
|
|
148
|
+
restore()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("checks again when the source changes to a photo that is already cached", () => {
|
|
153
|
+
const { container, rerender } = render(<Avatar src="/a.png" name="Layla Hassan" />)
|
|
154
|
+
fireEvent.load(container.querySelector("img")!)
|
|
155
|
+
const restore = stub(true, 64)
|
|
156
|
+
try {
|
|
157
|
+
rerender(<Avatar src="/cached.png" name="Vera Brandt" />)
|
|
158
|
+
expect(screen.getByRole("img", { name: "Vera Brandt" })).toHaveAttribute("src", "/cached.png")
|
|
159
|
+
} finally {
|
|
160
|
+
restore()
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("falls back to initials when the photo fails", () => {
|
|
166
|
+
const { frame } = avatar({ src: "/missing.png", name: "Layla Hassan" })
|
|
167
|
+
fireEvent.error(frame.querySelector("img")!)
|
|
168
|
+
expect(screen.queryByRole("img")).toBeNull()
|
|
169
|
+
expect(frame).toHaveClass("bg-muted")
|
|
170
|
+
expect(frame.textContent).toContain("LH")
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("shows the user icon when there is no name either", () => {
|
|
174
|
+
const { frame } = avatar()
|
|
175
|
+
const svg = frame.querySelector("svg")!
|
|
176
|
+
expect(svg).toHaveAttribute("aria-hidden", "true")
|
|
177
|
+
expect(svg).toHaveClass("size-3/4")
|
|
178
|
+
expect(svg).toHaveAttribute("stroke-width", "1.5")
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("falls back to the icon when the photo fails and there is no name", () => {
|
|
182
|
+
const { frame } = avatar({ src: "/missing.png" })
|
|
183
|
+
fireEvent.error(frame.querySelector("img")!)
|
|
184
|
+
expect(frame.querySelector("svg")).not.toBeNull()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe("variant=initials", () => {
|
|
189
|
+
it("shows the initials even when there is a photo", () => {
|
|
190
|
+
const { frame } = avatar({ variant: "initials", src: "/layla.png", name: "Layla Hassan" })
|
|
191
|
+
expect(screen.queryByRole("img")).toBeNull()
|
|
192
|
+
expect(frame).toHaveClass("bg-muted")
|
|
193
|
+
expect(frame.querySelector("[aria-hidden='true']")!.textContent).toBe("LH")
|
|
194
|
+
expect(frame.querySelector(".sr-only")!.textContent).toBe("Layla Hassan")
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it("shows the icon when there is no name to take initials from", () => {
|
|
198
|
+
const { frame } = avatar({ variant: "initials" })
|
|
199
|
+
expect(frame.querySelector("svg")).not.toBeNull()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe("variant=icon", () => {
|
|
204
|
+
it("shows the icon even with a photo and a name, and is still named by `name`", () => {
|
|
205
|
+
const { frame } = avatar({ variant: "icon", src: "/layla.png", name: "Layla Hassan" })
|
|
206
|
+
expect(screen.queryByRole("img")).toBeNull()
|
|
207
|
+
expect(frame.querySelector("svg")).not.toBeNull()
|
|
208
|
+
expect(frame.textContent).not.toContain("LH")
|
|
209
|
+
expect(frame.querySelector(".sr-only")!.textContent).toBe("Layla Hassan")
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it("names Figma's Style `variant`, so React's inline `style` still reaches the root", () => {
|
|
214
|
+
const { root } = avatar({ variant: "icon", style: { marginInlineStart: "1rem" } })
|
|
215
|
+
expect(root.style.marginInlineStart).toBe("1rem")
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it("passes the image attributes through to the <img>", () => {
|
|
219
|
+
avatar({
|
|
220
|
+
src: "/layla.png",
|
|
221
|
+
name: "Layla Hassan",
|
|
222
|
+
srcSet: "/layla.png 1x, /layla@2x.png 2x",
|
|
223
|
+
sizes: "32px",
|
|
224
|
+
loading: "lazy",
|
|
225
|
+
decoding: "async",
|
|
226
|
+
crossOrigin: "anonymous",
|
|
227
|
+
referrerPolicy: "no-referrer",
|
|
228
|
+
})
|
|
229
|
+
const img = document.querySelector<HTMLImageElement>("img[src='/layla.png']")!
|
|
230
|
+
expect(img).toHaveAttribute("srcset", "/layla.png 1x, /layla@2x.png 2x")
|
|
231
|
+
expect(img).toHaveAttribute("sizes", "32px")
|
|
232
|
+
expect(img).toHaveAttribute("loading", "lazy")
|
|
233
|
+
expect(img).toHaveAttribute("decoding", "async")
|
|
234
|
+
expect(img).toHaveAttribute("crossorigin", "anonymous")
|
|
235
|
+
expect(img).toHaveAttribute("referrerpolicy", "no-referrer")
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe("initials", () => {
|
|
239
|
+
const shown = (props: Partial<AvatarProps>) =>
|
|
240
|
+
avatar(props).frame.querySelector("[aria-hidden='true']")!.textContent
|
|
241
|
+
|
|
242
|
+
it("takes the first letters of the first and last words, upper-cased", () => {
|
|
243
|
+
expect(shown({ name: "layla ahmed hassan" })).toBe("LH")
|
|
244
|
+
expect(shown({ name: "Vera" })).toBe("V")
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("shows one letter at size 12, as Figma does", () => {
|
|
248
|
+
expect(shown({ name: "Vera Brandt", size: "12" })).toBe("V")
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("keeps two Arabic initials in their separate forms", () => {
|
|
252
|
+
expect(shown({ name: "ليلى حسن" })).toBe("لح")
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("hides the letters and names the fallback with `name`", () => {
|
|
256
|
+
const { frame } = avatar({ name: "Layla Hassan" })
|
|
257
|
+
expect(frame.querySelector(".sr-only")!.textContent).toBe("Layla Hassan")
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("is silent when `name` is empty, for when a label already names the person", () => {
|
|
261
|
+
const { frame } = avatar({ name: "" })
|
|
262
|
+
expect(frame.querySelector(".sr-only")).toBeNull()
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe("status", () => {
|
|
267
|
+
const dot = (props: Partial<AvatarProps>) =>
|
|
268
|
+
avatar({ status: "online", ...props }).root.lastElementChild as HTMLElement
|
|
269
|
+
|
|
270
|
+
it("is off unless set", () => {
|
|
271
|
+
const { root } = avatar({ name: "Layla" })
|
|
272
|
+
expect(root.children).toHaveLength(1)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it("uses the 8px dot at 32 and 24, and the 4px dot at 12", () => {
|
|
276
|
+
expect(dot({ size: "32" })).toHaveClass("size-2", "ring-2")
|
|
277
|
+
expect(dot({ size: "24" })).toHaveClass("size-2", "ring-2")
|
|
278
|
+
expect(dot({ size: "12" })).toHaveClass("size-1", "ring-1")
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("hangs 2px past the end corner on Standard 32 and 24", () => {
|
|
282
|
+
expect(dot({ size: "32" })).toHaveClass("absolute", "-bottom-0.5", "-end-0.5")
|
|
283
|
+
expect(dot({ size: "24" })).toHaveClass("-bottom-0.5", "-end-0.5")
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it("sits flush on Rounded and at 12", () => {
|
|
287
|
+
expect(dot({ radius: "rounded" })).toHaveClass("bottom-0", "end-0")
|
|
288
|
+
expect(dot({ size: "12" })).toHaveClass("bottom-0", "end-0")
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it("uses logical `end`, so dir=rtl moves it to the bottom-left", () => {
|
|
292
|
+
expect(SOURCE).not.toMatch(/\b-?(right|left)-/)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it.each(["online", "away", "busy", "offline"] as const)("passes %s through", (status) => {
|
|
296
|
+
const fills = { online: "bg-success", away: "bg-warning", busy: "bg-destructive", offline: "bg-muted" }
|
|
297
|
+
expect(dot({ status })).toHaveClass(fills[status])
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it("silences the dot with statusLabel=\"\", as name=\"\" silences the avatar", () => {
|
|
301
|
+
expect(dot({ statusLabel: "" }).textContent).toBe("")
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("takes a statusLabel for the dot's name", () => {
|
|
305
|
+
expect(dot({ statusLabel: "Layla is online" }).textContent).toBe("Layla is online")
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it("uses tokens only — no raw colour or pixel values", () => {
|
|
310
|
+
expect(SOURCE).not.toMatch(/#[0-9a-f]{3,8}\b|\[\d+px\]|rgb\(/i)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("merges a consumer className on the root", () => {
|
|
314
|
+
expect(avatar({ className: "shadow" }).root).toHaveClass("shadow", "size-8")
|
|
315
|
+
})
|
|
316
|
+
})
|