gbs-add-block 2.0.4 → 2.3.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/.gbs/skills/gbs-components/SKILL.md +190 -134
- package/.gbs/skills/gbs-components/references/install.md +25 -3
- package/.gbs/skills/gbs-components/references/styling.md +246 -201
- package/CHANGELOG.md +74 -0
- package/README.md +157 -11
- package/index.cjs +212 -3
- package/package.json +41 -10
- package/schema/passport-v1.schema.json +204 -0
- package/source/beta-components/accordion/passport.json +259 -0
- package/source/beta-components/accordion/styles.css +207 -208
- package/source/beta-components/alert/passport.json +250 -0
- package/source/beta-components/alert/styles.css +154 -155
- package/source/beta-components/avatar/passport.json +294 -0
- package/source/beta-components/avatar/styles.css +201 -203
- package/source/beta-components/badge/passport.json +332 -0
- package/source/beta-components/badge/styles.css +203 -204
- package/source/beta-components/breadcrumb/passport.json +243 -0
- package/source/beta-components/breadcrumb/styles.css +138 -139
- package/source/beta-components/button/passport.json +402 -0
- package/source/beta-components/button/passport.manual.json +31 -0
- package/source/beta-components/button/styles.css +232 -233
- package/source/beta-components/card/passport.json +337 -0
- package/source/beta-components/card/styles.css +230 -231
- package/source/beta-components/checkbox/passport.json +456 -0
- package/source/beta-components/checkbox/styles.css +211 -212
- package/source/beta-components/combobox/passport.json +456 -0
- package/source/beta-components/combobox/styles.css +419 -417
- package/source/beta-components/data-grid/agent/coerce.ts +368 -0
- package/source/beta-components/data-grid/agent/contract.ts +410 -0
- package/source/beta-components/data-grid/agent/dataset.ts +92 -0
- package/source/beta-components/data-grid/agent/engine.ts +470 -0
- package/source/beta-components/data-grid/agent/executors.ts +155 -0
- package/source/beta-components/data-grid/agent/index.ts +79 -0
- package/source/beta-components/data-grid/agent/intent.ts +324 -0
- package/source/beta-components/data-grid/agent/operations.ts +335 -0
- package/source/beta-components/data-grid/agent/validate.ts +630 -0
- package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
- package/source/beta-components/data-grid/index.ts +14 -7
- package/source/beta-components/data-grid/passport.json +1051 -0
- package/source/beta-components/data-grid/passport.manual.json +255 -0
- package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
- package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
- package/source/beta-components/data-grid/styles.css +874 -717
- package/source/beta-components/date-picker/passport.json +407 -0
- package/source/beta-components/date-picker/styles.css +445 -446
- package/source/beta-components/dialog/passport.json +344 -0
- package/source/beta-components/dialog/styles.css +280 -278
- package/source/beta-components/file-uploader/passport.json +518 -0
- package/source/beta-components/file-uploader/styles.css +394 -395
- package/source/beta-components/input/passport.json +536 -0
- package/source/beta-components/input/styles.css +295 -296
- package/source/beta-components/menu/passport.json +322 -0
- package/source/beta-components/menu/styles.css +224 -222
- package/source/beta-components/modal/passport.json +289 -0
- package/source/beta-components/modal/styles.css +241 -239
- package/source/beta-components/number-input/passport.json +541 -0
- package/source/beta-components/number-input/styles.css +230 -231
- package/source/beta-components/popover/passport.json +238 -0
- package/source/beta-components/popover/styles.css +148 -146
- package/source/beta-components/progress/passport.json +270 -0
- package/source/beta-components/progress/styles.css +200 -202
- package/source/beta-components/radio-group/passport.json +477 -0
- package/source/beta-components/radio-group/styles.css +241 -242
- package/source/beta-components/shared/core/agent/adapter.ts +65 -0
- package/source/beta-components/shared/core/agent/history.ts +120 -0
- package/source/beta-components/shared/core/agent/index.ts +46 -0
- package/source/beta-components/shared/core/agent/numbers.ts +217 -0
- package/source/beta-components/shared/core/agent/schema.ts +180 -0
- package/source/beta-components/shared/core/agent/types.ts +169 -0
- package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
- package/source/beta-components/shared/index.ts +9 -0
- package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
- package/source/beta-components/shared/react/useAskAgent.ts +217 -0
- package/source/beta-components/shared/styles.css +79 -0
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/passport.json +251 -0
- package/source/beta-components/skeleton/styles.css +185 -186
- package/source/beta-components/spinner/passport.json +245 -0
- package/source/beta-components/spinner/styles.css +145 -146
- package/source/beta-components/switch/passport.json +421 -0
- package/source/beta-components/switch/styles.css +194 -196
- package/source/beta-components/tabs/passport.json +315 -0
- package/source/beta-components/tabs/styles.css +234 -235
- package/source/beta-components/textarea/passport.json +382 -0
- package/source/beta-components/textarea/styles.css +158 -159
- package/source/beta-components/toaster/passport.json +221 -0
- package/source/beta-components/toaster/styles.css +282 -283
- package/source/beta-components/tooltip/passport.json +170 -0
- package/source/beta-components/tooltip/styles.css +71 -72
- package/tools/env.cjs +61 -0
- package/tools/passport/cli.cjs +79 -0
- package/tools/passport/extract.cjs +493 -0
- package/tools/passport/index.cjs +185 -0
- package/tools/passport/merge.cjs +131 -0
- package/tools/passport/policy.cjs +65 -0
- package/tools/passport/validate.cjs +277 -0
- package/tools/ts-require.cjs +79 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
- package/source/beta-components/alert/__tests__/core.test.ts +0 -17
- package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
- package/source/beta-components/badge/__tests__/core.test.ts +0 -46
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
- package/source/beta-components/button/__tests__/core.test.ts +0 -31
- package/source/beta-components/card/__tests__/core.test.ts +0 -57
- package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
- package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
- package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
- package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
- package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
- package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
- package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
- package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
- package/source/beta-components/input/__tests__/core.test.ts +0 -75
- package/source/beta-components/menu/__tests__/core.test.ts +0 -120
- package/source/beta-components/modal/__tests__/core.test.ts +0 -55
- package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
- package/source/beta-components/progress/__tests__/core.test.ts +0 -56
- package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
- package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
- package/source/beta-components/shared/__tests__/core.test.ts +0 -55
- package/source/beta-components/shared/__tests__/position.test.ts +0 -143
- package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
- package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
- package/source/beta-components/switch/__tests__/core.test.ts +0 -64
- package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
- package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
- package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
- package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { colorIndex, initials, splitGroup } from "../core/identity";
|
|
3
|
-
|
|
4
|
-
describe("initials", () => {
|
|
5
|
-
it("takes the first and last part of a name", () => {
|
|
6
|
-
expect(initials("Ada Lovelace")).toBe("AL");
|
|
7
|
-
expect(initials("Ada Byron King Lovelace")).toBe("AL");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("takes one letter from a single name, not two", () => {
|
|
11
|
-
// "MA" out of "Madonna" reads as a mistake.
|
|
12
|
-
expect(initials("Madonna")).toBe("M");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("splits on the punctuation that joins parts of a name", () => {
|
|
16
|
-
expect(initials("jean-luc picard")).toBe("JP");
|
|
17
|
-
expect(initials("ada.lovelace")).toBe("AL");
|
|
18
|
-
expect(initials("ada_lovelace")).toBe("AL");
|
|
19
|
-
expect(initials("Ada Lovelace")).toBe("AL");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("reads an email as the part before the at sign", () => {
|
|
23
|
-
// Nothing in "example.com" is this person's name.
|
|
24
|
-
expect(initials("ada@example.com")).toBe("A");
|
|
25
|
-
expect(initials("ada.lovelace@example.com")).toBe("AL");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("keeps a whole character, not half a surrogate pair", () => {
|
|
29
|
-
expect(initials("\u{1F680} Launch")).toBe("\u{1F680}L");
|
|
30
|
-
expect(Array.from(initials("\u{1F680}")).length).toBe(1);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("leaves scripts without letter case alone", () => {
|
|
34
|
-
// Uppercasing means nothing here, and can change the character.
|
|
35
|
-
expect(initials("山田 太郎")).toBe("山太");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("does not turn one letter into two", () => {
|
|
39
|
-
// Uppercase ß is SS, which would be two letters in a one-letter slot.
|
|
40
|
-
expect(initials("ßrian")).toBe("ß");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("has nothing for nothing", () => {
|
|
44
|
-
expect(initials("")).toBe("");
|
|
45
|
-
expect(initials(" ")).toBe("");
|
|
46
|
-
expect(initials("@example.com")).toBe("");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("colorIndex", () => {
|
|
51
|
-
it("gives the same person the same colour every time", () => {
|
|
52
|
-
expect(colorIndex("Ada Lovelace", 8)).toBe(colorIndex("Ada Lovelace", 8));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("stays inside the palette", () => {
|
|
56
|
-
for (const name of ["", "a", "Ada Lovelace", "\u{1F680}", "x".repeat(500)]) {
|
|
57
|
-
const index = colorIndex(name, 8);
|
|
58
|
-
expect(index).toBeGreaterThanOrEqual(0);
|
|
59
|
-
expect(index).toBeLessThan(8);
|
|
60
|
-
}
|
|
61
|
-
expect(colorIndex("Ada", 0)).toBe(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("spreads names across the palette rather than piling them up", () => {
|
|
65
|
-
const names = ["Ada", "Grace", "Alan", "Katherine", "Edsger", "Barbara", "Tim", "Linus"];
|
|
66
|
-
const used = new Set(names.map((name) => colorIndex(name, 8)));
|
|
67
|
-
expect(used.size).toBeGreaterThanOrEqual(5);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("splitGroup", () => {
|
|
72
|
-
const people = ["a", "b", "c", "d", "e"];
|
|
73
|
-
|
|
74
|
-
it("shows everyone when they fit", () => {
|
|
75
|
-
expect(splitGroup(people, 5)).toEqual({ shown: people, overflow: 0 });
|
|
76
|
-
expect(splitGroup(people, 9).overflow).toBe(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("counts the overflow bubble as one of the slots", () => {
|
|
80
|
-
// Four slots: three faces and a "+2", so the group stays four wide.
|
|
81
|
-
expect(splitGroup(people, 4)).toEqual({ shown: ["a", "b", "c"], overflow: 2 });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("handles a group with no room for faces", () => {
|
|
85
|
-
expect(splitGroup(people, 1)).toEqual({ shown: [], overflow: 5 });
|
|
86
|
-
expect(splitGroup(people, 0)).toEqual({ shown: [], overflow: 5 });
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatCount, showCount } from "../core/count";
|
|
3
|
-
|
|
4
|
-
describe("formatCount", () => {
|
|
5
|
-
it("prints the number while it fits", () => {
|
|
6
|
-
expect(formatCount(0)).toEqual({ text: "0" });
|
|
7
|
-
expect(formatCount(99)).toEqual({ text: "99" });
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("caps it, and says aloud what the cap means", () => {
|
|
11
|
-
// "99+" read out as "ninety-nine plus" is a guess at a number.
|
|
12
|
-
expect(formatCount(100)).toEqual({ text: "99+", spoken: "more than 99" });
|
|
13
|
-
expect(formatCount(4821, 999)).toEqual({ text: "999+", spoken: "more than 999" });
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("takes a formatter, for grouped or localized numbers", () => {
|
|
17
|
-
const format = new Intl.NumberFormat("de-DE").format;
|
|
18
|
-
expect(formatCount(1234, 9999, format).text).toBe("1.234");
|
|
19
|
-
expect(formatCount(20000, 9999, format)).toEqual({
|
|
20
|
-
text: "9.999+",
|
|
21
|
-
spoken: "more than 9.999",
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("drops the fraction of a number that should not have one", () => {
|
|
26
|
-
expect(formatCount(3.7).text).toBe("3");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("has nothing to print for a number that is not one", () => {
|
|
30
|
-
expect(formatCount(Number.NaN).text).toBe("");
|
|
31
|
-
expect(formatCount(Number.POSITIVE_INFINITY).text).toBe("");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("showCount", () => {
|
|
36
|
-
it("leaves a zero off the page unless it is asked for", () => {
|
|
37
|
-
expect(showCount(0)).toBe(false);
|
|
38
|
-
expect(showCount(0, true)).toBe(true);
|
|
39
|
-
expect(showCount(1)).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("never shows a negative or a non-number", () => {
|
|
43
|
-
expect(showCount(-1)).toBe(false);
|
|
44
|
-
expect(showCount(Number.NaN, true)).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { collapseItems, toBreadcrumbJsonLd } from "../core/trail";
|
|
3
|
-
|
|
4
|
-
const shape = (slots: ReturnType<typeof collapseItems>) =>
|
|
5
|
-
slots.map((slot) => (slot.kind === "item" ? slot.index : `…${slot.hidden.join(",")}`));
|
|
6
|
-
|
|
7
|
-
describe("collapsing", () => {
|
|
8
|
-
it("leaves a short trail alone", () => {
|
|
9
|
-
expect(shape(collapseItems(3))).toEqual([0, 1, 2]);
|
|
10
|
-
expect(shape(collapseItems(4, 4))).toEqual([0, 1, 2, 3]);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("keeps the first and last crumbs around an ellipsis", () => {
|
|
14
|
-
expect(shape(collapseItems(6, 4))).toEqual([0, "…1,2,3,4", 5]);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("keeps more on either side when asked", () => {
|
|
18
|
-
expect(shape(collapseItems(7, 4, 1, 2))).toEqual([0, "…1,2,3,4", 5, 6]);
|
|
19
|
-
expect(shape(collapseItems(7, 4, 2, 2))).toEqual([0, 1, "…2,3,4", 5, 6]);
|
|
20
|
-
expect(shape(collapseItems(7, 4, 0, 1))).toEqual(["…0,1,2,3,4,5", 6]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("doesn't collapse when the kept crumbs already cover the trail", () => {
|
|
24
|
-
expect(shape(collapseItems(4, 2, 2, 2))).toEqual([0, 1, 2, 3]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("always keeps the current page", () => {
|
|
28
|
-
expect(shape(collapseItems(5, 2, 1, 0))).toEqual([0, "…1,2,3", 4]);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("structured data", () => {
|
|
33
|
-
it("builds a schema.org BreadcrumbList with absolute links", () => {
|
|
34
|
-
const data = toBreadcrumbJsonLd(
|
|
35
|
-
[
|
|
36
|
-
{ name: "Home", href: "/" },
|
|
37
|
-
{ name: "Invoices", href: "/invoices" },
|
|
38
|
-
{ name: "INV-042" },
|
|
39
|
-
],
|
|
40
|
-
"https://app.example.com/billing/",
|
|
41
|
-
);
|
|
42
|
-
expect(data).toEqual({
|
|
43
|
-
"@context": "https://schema.org",
|
|
44
|
-
"@type": "BreadcrumbList",
|
|
45
|
-
itemListElement: [
|
|
46
|
-
{ "@type": "ListItem", position: 1, name: "Home", item: "https://app.example.com/" },
|
|
47
|
-
{ "@type": "ListItem", position: 2, name: "Invoices", item: "https://app.example.com/invoices" },
|
|
48
|
-
{ "@type": "ListItem", position: 3, name: "INV-042" },
|
|
49
|
-
],
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("keeps links as given without a base URL", () => {
|
|
54
|
-
expect(toBreadcrumbJsonLd([{ name: "Docs", href: "https://example.com/docs" }]).itemListElement[0].item).toBe(
|
|
55
|
-
"https://example.com/docs",
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { buttonState, isPromiseLike } from "../core/state";
|
|
3
|
-
|
|
4
|
-
describe("button state", () => {
|
|
5
|
-
it("is active by default", () => {
|
|
6
|
-
expect(buttonState({})).toEqual({ busy: false, inactive: false, nativeDisabled: false });
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("stays focusable while busy", () => {
|
|
10
|
-
expect(buttonState({ loading: true })).toEqual({ busy: true, inactive: true, nativeDisabled: false });
|
|
11
|
-
expect(buttonState({ pending: true })).toEqual({ busy: true, inactive: true, nativeDisabled: false });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("uses native disabled, and shows no spinner, when disabled", () => {
|
|
15
|
-
expect(buttonState({ disabled: true, loading: true })).toEqual({
|
|
16
|
-
busy: false,
|
|
17
|
-
inactive: true,
|
|
18
|
-
nativeDisabled: true,
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("promise detection", () => {
|
|
24
|
-
it("recognizes promises and thenables only", () => {
|
|
25
|
-
expect(isPromiseLike(Promise.resolve())).toBe(true);
|
|
26
|
-
expect(isPromiseLike({ then: () => {} })).toBe(true);
|
|
27
|
-
expect(isPromiseLike(undefined)).toBe(false);
|
|
28
|
-
expect(isPromiseLike({ then: 1 })).toBe(false);
|
|
29
|
-
expect(isPromiseLike(() => {})).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { isPositive, percentChange, trendDirection } from "../core/trend";
|
|
3
|
-
|
|
4
|
-
describe("trendDirection", () => {
|
|
5
|
-
it("reads the sign of the change", () => {
|
|
6
|
-
expect(trendDirection(4.2)).toBe("up");
|
|
7
|
-
expect(trendDirection(-0.3)).toBe("down");
|
|
8
|
-
expect(trendDirection(0)).toBe("flat");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("treats movement under the threshold as flat", () => {
|
|
12
|
-
expect(trendDirection(0.4, 0.5)).toBe("flat");
|
|
13
|
-
expect(trendDirection(-0.4, 0.5)).toBe("flat");
|
|
14
|
-
expect(trendDirection(0.6, 0.5)).toBe("up");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("is flat for a value that is not a number", () => {
|
|
18
|
-
expect(trendDirection(Number.NaN)).toBe("flat");
|
|
19
|
-
expect(trendDirection(Number.POSITIVE_INFINITY)).toBe("flat");
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("isPositive", () => {
|
|
24
|
-
it("treats up as good by default", () => {
|
|
25
|
-
expect(isPositive("up")).toBe(true);
|
|
26
|
-
expect(isPositive("down")).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("flips for figures where down is the good outcome, such as churn", () => {
|
|
30
|
-
expect(isPositive("up", true)).toBe(false);
|
|
31
|
-
expect(isPositive("down", true)).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("is null when nothing moved, so no colour is applied", () => {
|
|
35
|
-
expect(isPositive("flat")).toBeNull();
|
|
36
|
-
expect(isPositive("flat", true)).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("percentChange", () => {
|
|
41
|
-
it("is the change as a share of where it started", () => {
|
|
42
|
-
expect(percentChange(200, 250)).toBe(25);
|
|
43
|
-
expect(percentChange(200, 150)).toBe(-25);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("measures against the size of a negative start", () => {
|
|
47
|
-
expect(percentChange(-200, -150)).toBe(25);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("is null from zero, which has no percentage", () => {
|
|
51
|
-
expect(percentChange(0, 10)).toBeNull();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("is null for values that are not numbers", () => {
|
|
55
|
-
expect(percentChange(Number.NaN, 10)).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { groupState, toggleAll, toggleValue } from "../core/group";
|
|
3
|
-
|
|
4
|
-
const options = [
|
|
5
|
-
{ value: "email" },
|
|
6
|
-
{ value: "sms" },
|
|
7
|
-
{ value: "push", disabled: true },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
describe("toggling one value", () => {
|
|
11
|
-
it("adds and removes without duplicates", () => {
|
|
12
|
-
expect(toggleValue(["email"], "sms", true)).toEqual(["email", "sms"]);
|
|
13
|
-
expect(toggleValue(["email"], "email", true)).toEqual(["email"]);
|
|
14
|
-
expect(toggleValue(["email", "sms"], "email", false)).toEqual(["sms"]);
|
|
15
|
-
expect(toggleValue([], "email", false)).toEqual([]);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe("select all", () => {
|
|
20
|
-
it("reports none, some or all", () => {
|
|
21
|
-
expect(groupState([], options)).toBe(false);
|
|
22
|
-
expect(groupState(["sms"], options)).toBe("indeterminate");
|
|
23
|
-
expect(groupState(["email", "sms", "push"], options)).toBe(true);
|
|
24
|
-
expect(groupState(["email"], [])).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("selects every enabled option, leaving disabled ones alone", () => {
|
|
28
|
-
expect(toggleAll(["sms"], options)).toEqual(["sms", "email"]);
|
|
29
|
-
expect(toggleAll([], options)).toEqual(["email", "sms"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("clears enabled options once they are all selected", () => {
|
|
33
|
-
expect(toggleAll(["email", "sms"], options)).toEqual([]);
|
|
34
|
-
expect(toggleAll(["push", "email", "sms"], options)).toEqual(["push"]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("keeps values that aren't options", () => {
|
|
38
|
-
expect(toggleAll(["legacy"], options)).toEqual(["legacy", "email", "sms"]);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildListItems,
|
|
4
|
-
filterOptions,
|
|
5
|
-
findByPrefix,
|
|
6
|
-
firstEnabledIndex,
|
|
7
|
-
lastEnabledIndex,
|
|
8
|
-
matchRanges,
|
|
9
|
-
nextEnabledIndex,
|
|
10
|
-
toggleValue,
|
|
11
|
-
} from "../core/filter";
|
|
12
|
-
import type { ComboboxOption } from "../core/types";
|
|
13
|
-
import { getVisibleRange, measureItems, scrollToItem } from "../core/virtual";
|
|
14
|
-
|
|
15
|
-
const options: ComboboxOption[] = [
|
|
16
|
-
{ value: "in", label: "India", group: "Asia", keywords: ["bharat"] },
|
|
17
|
-
{ value: "jp", label: "Japan", group: "Asia" },
|
|
18
|
-
{ value: "kr", label: "South Korea", group: "Asia", disabled: true },
|
|
19
|
-
{ value: "de", label: "Germany", group: "Europe", description: "Berlin" },
|
|
20
|
-
{ value: "fr", label: "France", group: "Europe" },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const labels = (list: { option: ComboboxOption }[]) => list.map((entry) => entry.option.label);
|
|
24
|
-
|
|
25
|
-
describe("matching", () => {
|
|
26
|
-
it("returns merged, case-insensitive ranges for every term", () => {
|
|
27
|
-
expect(matchRanges("South Korea", ["korea"])).toEqual([[6, 11]]);
|
|
28
|
-
expect(matchRanges("Germany", ["ger", "man"])).toEqual([[0, 6]]);
|
|
29
|
-
expect(matchRanges("Germany", ["ger", "xyz"])).toBeNull();
|
|
30
|
-
expect(matchRanges("Germany", [])).toEqual([]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("filters on label, description and keywords", () => {
|
|
34
|
-
expect(labels(filterOptions(options, ""))).toHaveLength(5);
|
|
35
|
-
expect(labels(filterOptions(options, "an"))).toEqual(["Japan", "Germany", "France"]);
|
|
36
|
-
expect(labels(filterOptions(options, "berlin"))).toEqual(["Germany"]);
|
|
37
|
-
expect(labels(filterOptions(options, "bharat"))).toEqual(["India"]);
|
|
38
|
-
expect(labels(filterOptions(options, "nothing"))).toEqual([]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("highlights the label only", () => {
|
|
42
|
-
const [india] = filterOptions(options, "ind");
|
|
43
|
-
expect(india.matches).toEqual([[0, 3]]);
|
|
44
|
-
const [germany] = filterOptions(options, "berlin");
|
|
45
|
-
expect(germany.matches).toEqual([]);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("uses a custom filter when given", () => {
|
|
49
|
-
const entries = filterOptions(options, "x", (option) => option.value === "jp");
|
|
50
|
-
expect(labels(entries)).toEqual(["Japan"]);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("list building", () => {
|
|
55
|
-
it("adds group headings and numbers options across groups", () => {
|
|
56
|
-
const items = buildListItems(filterOptions(options, ""));
|
|
57
|
-
expect(items.map((item) => (item.kind === "group" ? `# ${item.label}` : item.entry.option.label))).toEqual([
|
|
58
|
-
"# Asia",
|
|
59
|
-
"India",
|
|
60
|
-
"Japan",
|
|
61
|
-
"South Korea",
|
|
62
|
-
"# Europe",
|
|
63
|
-
"Germany",
|
|
64
|
-
"France",
|
|
65
|
-
]);
|
|
66
|
-
const optionItems = items.filter((item) => item.kind === "option");
|
|
67
|
-
expect(optionItems.map((item) => (item.kind === "option" ? item.index : -1))).toEqual([0, 1, 2, 3, 4]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("skips headings when no option has a group", () => {
|
|
71
|
-
const flat = buildListItems(filterOptions([{ value: "a", label: "A" }], ""));
|
|
72
|
-
expect(flat).toHaveLength(1);
|
|
73
|
-
expect(flat[0].kind).toBe("option");
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("keyboard helpers", () => {
|
|
78
|
-
const entries = filterOptions(options, "");
|
|
79
|
-
|
|
80
|
-
it("moves over enabled options and wraps", () => {
|
|
81
|
-
expect(nextEnabledIndex(entries, 0, 1)).toBe(1);
|
|
82
|
-
expect(nextEnabledIndex(entries, 1, 1)).toBe(3); // skips the disabled option
|
|
83
|
-
expect(nextEnabledIndex(entries, 4, 1)).toBe(0);
|
|
84
|
-
expect(nextEnabledIndex(entries, 0, -1)).toBe(4);
|
|
85
|
-
expect(nextEnabledIndex([], 0, 1)).toBe(-1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("finds the first and last enabled option", () => {
|
|
89
|
-
expect(firstEnabledIndex(entries)).toBe(0);
|
|
90
|
-
expect(firstEnabledIndex(entries, 2)).toBe(3);
|
|
91
|
-
expect(lastEnabledIndex(entries)).toBe(4);
|
|
92
|
-
expect(lastEnabledIndex([{ option: options[2], matches: [] }])).toBe(-1);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("jumps to a typed prefix", () => {
|
|
96
|
-
expect(findByPrefix(entries, "ja", -1)).toBe(1);
|
|
97
|
-
expect(findByPrefix(entries, "s", -1)).toBe(-1); // South Korea is disabled
|
|
98
|
-
expect(findByPrefix(entries, "zz", -1)).toBe(-1);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("selection", () => {
|
|
103
|
-
it("adds, removes and respects max", () => {
|
|
104
|
-
expect(toggleValue(["a"], "b")).toEqual(["a", "b"]);
|
|
105
|
-
expect(toggleValue(["a", "b"], "a")).toEqual(["b"]);
|
|
106
|
-
expect(toggleValue(["a", "b"], "c", 2)).toEqual(["a", "b"]);
|
|
107
|
-
expect(toggleValue(["a", "b"], "b", 2)).toEqual(["a"]);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("virtualization", () => {
|
|
112
|
-
const items = buildListItems(filterOptions(options, ""));
|
|
113
|
-
const metrics = measureItems(items, { option: 34, group: 26 });
|
|
114
|
-
|
|
115
|
-
it("measures group and option rows", () => {
|
|
116
|
-
expect(metrics.total).toBe(2 * 26 + 5 * 34);
|
|
117
|
-
expect(metrics.offsets[0]).toBe(0);
|
|
118
|
-
expect(metrics.offsets[1]).toBe(26);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("returns the visible window with overscan", () => {
|
|
122
|
-
expect(getVisibleRange(metrics, 0, 60, 0)).toEqual({ start: 0, end: 3 });
|
|
123
|
-
expect(getVisibleRange(metrics, 0, 0, 0)).toEqual({ start: 0, end: 7 });
|
|
124
|
-
const range = getVisibleRange(metrics, 100, 60, 1);
|
|
125
|
-
expect(range.start).toBeLessThanOrEqual(3);
|
|
126
|
-
expect(range.end).toBeGreaterThanOrEqual(5);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("scrolls an item into view only when needed", () => {
|
|
130
|
-
expect(scrollToItem(metrics, 0, 0, 100)).toBeNull();
|
|
131
|
-
expect(scrollToItem(metrics, 6, 0, 100)).toBe(metrics.offsets[7] - 100);
|
|
132
|
-
expect(scrollToItem(metrics, 1, 40, 100)).toBe(26);
|
|
133
|
-
});
|
|
134
|
-
});
|