@fpkit/acss 0.5.13 → 0.6.1
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/libs/{chunk-PQ2K3BM6.cjs → chunk-2NRIP6RB.cjs} +3 -3
- package/libs/chunk-33PNJ4LO.cjs +15 -0
- package/libs/chunk-33PNJ4LO.cjs.map +1 -0
- package/libs/chunk-4BZKFPEC.cjs +17 -0
- package/libs/chunk-4BZKFPEC.cjs.map +1 -0
- package/libs/{chunk-772NRB75.js → chunk-5QD3DWFI.js} +2 -2
- package/libs/chunk-6SAHIYCZ.js +7 -0
- package/libs/chunk-6SAHIYCZ.js.map +1 -0
- package/libs/{chunk-3MKLDCKQ.cjs → chunk-6WTC4JXH.cjs} +3 -3
- package/libs/chunk-75QHTLFO.js +7 -0
- package/libs/chunk-75QHTLFO.js.map +1 -0
- package/libs/{chunk-ZANSFMTD.js → chunk-7XPFW7CB.js} +3 -3
- package/libs/chunk-BFK62VX5.js +5 -0
- package/libs/chunk-BFK62VX5.js.map +1 -0
- package/libs/{chunk-ROZI23GS.cjs → chunk-DKTHCQ5P.cjs} +4 -4
- package/libs/chunk-E2AJURUW.cjs +13 -0
- package/libs/chunk-E2AJURUW.cjs.map +1 -0
- package/libs/{chunk-L75OQKEI.cjs → chunk-ENTCUJ3A.cjs} +3 -3
- package/libs/chunk-ENTCUJ3A.cjs.map +1 -0
- package/libs/chunk-F5EYMVQM.js +10 -0
- package/libs/chunk-F5EYMVQM.js.map +1 -0
- package/libs/chunk-FVROL3V5.js +9 -0
- package/libs/chunk-FVROL3V5.js.map +1 -0
- package/libs/chunk-GT77BX4L.cjs +17 -0
- package/libs/chunk-GT77BX4L.cjs.map +1 -0
- package/libs/chunk-GUJSMQ3V.cjs +16 -0
- package/libs/chunk-GUJSMQ3V.cjs.map +1 -0
- package/libs/chunk-HHLNOC5T.js +7 -0
- package/libs/chunk-HHLNOC5T.js.map +1 -0
- package/libs/chunk-HRRHPLER.js +8 -0
- package/libs/chunk-HRRHPLER.js.map +1 -0
- package/libs/chunk-IEB64SWY.js +8 -0
- package/libs/chunk-IEB64SWY.js.map +1 -0
- package/libs/{chunk-NGTJDDFO.js → chunk-IQ76HGVP.js} +2 -2
- package/libs/chunk-IRLFZ3OL.js +9 -0
- package/libs/chunk-IRLFZ3OL.js.map +1 -0
- package/libs/{chunk-JJ43O4Y5.js → chunk-KK47SYZI.js} +2 -2
- package/libs/chunk-O3JIHC5M.cjs +15 -0
- package/libs/chunk-O3JIHC5M.cjs.map +1 -0
- package/libs/chunk-O5XAJ7BY.cjs +18 -0
- package/libs/chunk-O5XAJ7BY.cjs.map +1 -0
- package/libs/chunk-OVWLQYMK.js +10 -0
- package/libs/chunk-OVWLQYMK.js.map +1 -0
- package/libs/chunk-PNWIRCG3.cjs +7 -0
- package/libs/chunk-PNWIRCG3.cjs.map +1 -0
- package/libs/{chunk-D4YLRWAO.cjs → chunk-QVW6W76L.cjs} +6 -6
- package/libs/chunk-T4T6GWYQ.cjs +17 -0
- package/libs/chunk-T4T6GWYQ.cjs.map +1 -0
- package/libs/chunk-TON2YGMD.cjs +9 -0
- package/libs/chunk-TON2YGMD.cjs.map +1 -0
- package/libs/chunk-UEPAWMDF.js +8 -0
- package/libs/chunk-UEPAWMDF.js.map +1 -0
- package/libs/{chunk-LT5KZ2QW.cjs → chunk-US2I5GI7.cjs} +3 -3
- package/libs/{chunk-B7F5FS6D.cjs → chunk-W2UIN7EV.cjs} +3 -3
- package/libs/{chunk-P2DC76ZZ.cjs → chunk-W5TKWBFC.cjs} +3 -3
- package/libs/chunk-WXBFBWYF.cjs +16 -0
- package/libs/chunk-WXBFBWYF.cjs.map +1 -0
- package/libs/{chunk-VUH3FXGJ.js → chunk-X3JCTEPD.js} +5 -5
- package/libs/chunk-X5LGFCWG.js +9 -0
- package/libs/chunk-X5LGFCWG.js.map +1 -0
- package/libs/{chunk-5M57K4SW.js → chunk-Y2PFDELK.js} +2 -2
- package/libs/{chunk-ETFLFC2S.js → chunk-ZFJ4U45S.js} +2 -2
- package/libs/{component-props-a8a2f97e.d.ts → component-props-67d978a2.d.ts} +4 -4
- package/libs/components/alert/alert.css +1 -1
- package/libs/components/alert/alert.css.map +1 -1
- package/libs/components/alert/alert.min.css +2 -2
- package/libs/components/breadcrumbs/breadcrumb.cjs +6 -6
- package/libs/components/breadcrumbs/breadcrumb.d.cts +11 -11
- package/libs/components/breadcrumbs/breadcrumb.d.ts +11 -11
- package/libs/components/breadcrumbs/breadcrumb.js +3 -3
- package/libs/components/button.cjs +6 -4
- package/libs/components/button.d.cts +97 -4
- package/libs/components/button.d.ts +97 -4
- package/libs/components/button.js +4 -2
- package/libs/components/card.cjs +7 -7
- package/libs/components/card.d.cts +14 -14
- package/libs/components/card.d.ts +14 -14
- package/libs/components/card.js +2 -2
- package/libs/components/dialog/dialog.cjs +9 -7
- package/libs/components/dialog/dialog.d.cts +3 -3
- package/libs/components/dialog/dialog.d.ts +3 -3
- package/libs/components/dialog/dialog.js +7 -5
- package/libs/components/form/fields.cjs +4 -4
- package/libs/components/form/fields.d.cts +16 -7
- package/libs/components/form/fields.d.ts +16 -7
- package/libs/components/form/fields.js +2 -2
- package/libs/components/form/inputs.cjs +6 -4
- package/libs/components/form/inputs.d.cts +50 -2
- package/libs/components/form/inputs.d.ts +50 -2
- package/libs/components/form/inputs.js +4 -2
- package/libs/components/form/textarea.cjs +5 -4
- package/libs/components/form/textarea.d.cts +32 -23
- package/libs/components/form/textarea.d.ts +32 -23
- package/libs/components/form/textarea.js +3 -2
- package/libs/components/heading/heading.cjs +3 -3
- package/libs/components/heading/heading.d.cts +2 -2
- package/libs/components/heading/heading.d.ts +2 -2
- package/libs/components/heading/heading.js +2 -2
- package/libs/components/icons/icon.cjs +4 -4
- package/libs/components/icons/icon.d.cts +38 -38
- package/libs/components/icons/icon.d.ts +38 -38
- package/libs/components/icons/icon.js +2 -2
- package/libs/components/link/link.cjs +4 -4
- package/libs/components/link/link.css +1 -1
- package/libs/components/link/link.css.map +1 -1
- package/libs/components/link/link.d.cts +3 -19
- package/libs/components/link/link.d.ts +3 -19
- package/libs/components/link/link.js +2 -2
- package/libs/components/link/link.min.css +2 -2
- package/libs/components/list/list.cjs +5 -5
- package/libs/components/list/list.css +1 -0
- package/libs/components/list/list.css.map +1 -0
- package/libs/components/list/list.d.cts +120 -33
- package/libs/components/list/list.d.ts +120 -33
- package/libs/components/list/list.js +2 -2
- package/libs/components/list/list.min.css +3 -0
- package/libs/components/modal.cjs +6 -4
- package/libs/components/modal.d.cts +8 -8
- package/libs/components/modal.d.ts +8 -8
- package/libs/components/modal.js +5 -3
- package/libs/components/nav/nav.cjs +7 -7
- package/libs/components/nav/nav.css +1 -1
- package/libs/components/nav/nav.css.map +1 -1
- package/libs/components/nav/nav.d.cts +550 -34
- package/libs/components/nav/nav.d.ts +550 -34
- package/libs/components/nav/nav.js +3 -3
- package/libs/components/nav/nav.min.css +2 -2
- package/libs/components/popover/popover.d.cts +5 -5
- package/libs/components/popover/popover.d.ts +5 -5
- package/libs/components/tables/table.cjs +5 -5
- package/libs/components/tables/table.d.cts +8 -8
- package/libs/components/tables/table.d.ts +8 -8
- package/libs/components/tables/table.js +2 -2
- package/libs/components/tag/tag.css +1 -1
- package/libs/components/tag/tag.css.map +1 -1
- package/libs/components/tag/tag.min.css +2 -2
- package/libs/components/text/text.cjs +5 -5
- package/libs/components/text/text.d.cts +5 -5
- package/libs/components/text/text.d.ts +5 -5
- package/libs/components/text/text.js +2 -2
- package/libs/form.types-d25ebfac.d.ts +233 -0
- package/libs/{heading-3648c538.d.ts → heading-7446cb46.d.ts} +8 -8
- package/libs/hooks.cjs +9 -4
- package/libs/hooks.d.cts +137 -3
- package/libs/hooks.d.ts +137 -3
- package/libs/hooks.js +4 -3
- package/libs/icons.cjs +3 -3
- package/libs/icons.d.cts +2 -2
- package/libs/icons.d.ts +2 -2
- package/libs/icons.js +2 -2
- package/libs/index.cjs +53 -51
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +338 -49
- package/libs/index.d.ts +338 -49
- package/libs/index.js +24 -22
- package/libs/index.js.map +1 -1
- package/libs/link-5192f411.d.ts +323 -0
- package/libs/list.types-d26de310.d.ts +245 -0
- package/libs/{ui-645f95b5.d.ts → ui-d01b50d4.d.ts} +16 -12
- package/package.json +4 -6
- package/src/components/alert/alert.scss +1 -4
- package/src/components/breadcrumbs/breadcrumb.tsx +4 -1
- package/src/components/buttons/README.mdx +102 -1
- package/src/components/buttons/button.stories.tsx +106 -0
- package/src/components/buttons/button.tsx +82 -52
- package/src/components/dialog/dialog-a11y-review.md +653 -0
- package/src/components/form/README.mdx +725 -43
- package/src/components/form/WCAG-REVIEW.md +654 -0
- package/src/components/form/fields.tsx +10 -1
- package/src/components/form/form.stories.tsx +604 -23
- package/src/components/form/form.tsx +204 -63
- package/src/components/form/form.types.ts +378 -0
- package/src/components/form/input.stories.tsx +71 -3
- package/src/components/form/inputs.tsx +159 -67
- package/src/components/form/select.tsx +122 -66
- package/src/components/form/textarea.tsx +120 -73
- package/src/components/fp.tsx +86 -11
- package/src/components/link/README.mdx +923 -0
- package/src/components/link/link.scss +79 -26
- package/src/components/link/link.stories.tsx +383 -30
- package/src/components/link/link.test.tsx +677 -0
- package/src/components/link/link.tsx +163 -57
- package/src/components/link/link.types.ts +261 -0
- package/src/components/list/README.mdx +764 -0
- package/src/components/list/list.scss +285 -0
- package/src/components/list/list.stories.tsx +514 -27
- package/src/components/list/list.test.tsx +554 -0
- package/src/components/list/list.tsx +153 -51
- package/src/components/list/list.types.ts +255 -0
- package/src/components/nav/ACCESSIBILITY.md +649 -0
- package/src/components/nav/README.mdx +782 -0
- package/src/components/nav/nav.scss +37 -4
- package/src/components/nav/nav.stories.tsx +44 -6
- package/src/components/nav/nav.tsx +302 -51
- package/src/components/nav/nav.types.ts +308 -0
- package/src/components/tag/README.mdx +426 -0
- package/src/components/tag/tag.scss +101 -27
- package/src/components/tag/tag.stories.tsx +384 -10
- package/src/components/tag/tag.test.tsx +210 -0
- package/src/components/tag/tag.tsx +106 -9
- package/src/components/tag/tag.types.ts +107 -0
- package/src/components/ui.tsx +8 -3
- package/src/hooks/use-disabled-state.test.tsx +536 -0
- package/src/hooks/use-disabled-state.ts +246 -0
- package/src/hooks/useDisabledState.md +393 -0
- package/src/hooks.ts +6 -0
- package/src/index.scss +2 -0
- package/src/index.ts +2 -1
- package/src/sass/_globals.scss +2 -7
- package/src/styles/alert/alert.css +1 -3
- package/src/styles/alert/alert.css.map +1 -1
- package/src/styles/index.css +461 -81
- package/src/styles/index.css.map +1 -1
- package/src/styles/link/link.css +45 -28
- package/src/styles/link/link.css.map +1 -1
- package/src/styles/list/list.css +214 -0
- package/src/styles/list/list.css.map +1 -0
- package/src/styles/nav/nav.css +32 -6
- package/src/styles/nav/nav.css.map +1 -1
- package/src/styles/tag/tag.css +113 -35
- package/src/styles/tag/tag.css.map +1 -1
- package/src/styles/utilities/_disabled.scss +58 -0
- package/src/types/shared.ts +43 -6
- package/src/utils/accessibility.ts +109 -0
- package/libs/chunk-2LTJ7HHX.cjs +0 -18
- package/libs/chunk-2LTJ7HHX.cjs.map +0 -1
- package/libs/chunk-2Y7W75TT.js +0 -9
- package/libs/chunk-2Y7W75TT.js.map +0 -1
- package/libs/chunk-5S4ORA4C.cjs +0 -15
- package/libs/chunk-5S4ORA4C.cjs.map +0 -1
- package/libs/chunk-AHDJGCG5.cjs +0 -15
- package/libs/chunk-AHDJGCG5.cjs.map +0 -1
- package/libs/chunk-BHRQBJRY.js +0 -8
- package/libs/chunk-BHRQBJRY.js.map +0 -1
- package/libs/chunk-GZ4QFPRY.js +0 -9
- package/libs/chunk-GZ4QFPRY.js.map +0 -1
- package/libs/chunk-IYUN2EW3.cjs +0 -15
- package/libs/chunk-IYUN2EW3.cjs.map +0 -1
- package/libs/chunk-J32EZPYD.cjs +0 -15
- package/libs/chunk-J32EZPYD.cjs.map +0 -1
- package/libs/chunk-KUKIVRC2.js +0 -7
- package/libs/chunk-KUKIVRC2.js.map +0 -1
- package/libs/chunk-L75OQKEI.cjs.map +0 -1
- package/libs/chunk-M5RRNTVX.cjs +0 -15
- package/libs/chunk-M5RRNTVX.cjs.map +0 -1
- package/libs/chunk-OK5QEIMD.cjs +0 -17
- package/libs/chunk-OK5QEIMD.cjs.map +0 -1
- package/libs/chunk-P7TTEYCD.js +0 -7
- package/libs/chunk-P7TTEYCD.js.map +0 -1
- package/libs/chunk-QLZWHAMK.js +0 -8
- package/libs/chunk-QLZWHAMK.js.map +0 -1
- package/libs/chunk-RIVUMPOG.js +0 -8
- package/libs/chunk-RIVUMPOG.js.map +0 -1
- package/libs/chunk-S7BABR7Z.cjs +0 -13
- package/libs/chunk-S7BABR7Z.cjs.map +0 -1
- package/libs/chunk-SMYRLO3E.js +0 -8
- package/libs/chunk-SMYRLO3E.js.map +0 -1
- package/libs/chunk-TYRCEX2L.js +0 -8
- package/libs/chunk-TYRCEX2L.js.map +0 -1
- package/libs/chunk-XBA562WW.js +0 -8
- package/libs/chunk-XBA562WW.js.map +0 -1
- package/libs/chunk-XTQKWY7W.cjs +0 -32
- package/libs/chunk-XTQKWY7W.cjs.map +0 -1
- package/libs/inputs-f3a216db.d.ts +0 -45
- /package/libs/{chunk-PQ2K3BM6.cjs.map → chunk-2NRIP6RB.cjs.map} +0 -0
- /package/libs/{chunk-772NRB75.js.map → chunk-5QD3DWFI.js.map} +0 -0
- /package/libs/{chunk-3MKLDCKQ.cjs.map → chunk-6WTC4JXH.cjs.map} +0 -0
- /package/libs/{chunk-ZANSFMTD.js.map → chunk-7XPFW7CB.js.map} +0 -0
- /package/libs/{chunk-ROZI23GS.cjs.map → chunk-DKTHCQ5P.cjs.map} +0 -0
- /package/libs/{chunk-NGTJDDFO.js.map → chunk-IQ76HGVP.js.map} +0 -0
- /package/libs/{chunk-JJ43O4Y5.js.map → chunk-KK47SYZI.js.map} +0 -0
- /package/libs/{chunk-D4YLRWAO.cjs.map → chunk-QVW6W76L.cjs.map} +0 -0
- /package/libs/{chunk-LT5KZ2QW.cjs.map → chunk-US2I5GI7.cjs.map} +0 -0
- /package/libs/{chunk-B7F5FS6D.cjs.map → chunk-W2UIN7EV.cjs.map} +0 -0
- /package/libs/{chunk-P2DC76ZZ.cjs.map → chunk-W5TKWBFC.cjs.map} +0 -0
- /package/libs/{chunk-VUH3FXGJ.js.map → chunk-X3JCTEPD.js.map} +0 -0
- /package/libs/{chunk-5M57K4SW.js.map → chunk-Y2PFDELK.js.map} +0 -0
- /package/libs/{chunk-ETFLFC2S.js.map → chunk-ZFJ4U45S.js.map} +0 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import Link from "./link";
|
|
6
|
+
|
|
7
|
+
describe("Link Component", () => {
|
|
8
|
+
describe("Rendering", () => {
|
|
9
|
+
it("should render with href and children", () => {
|
|
10
|
+
render(<Link href="/about">About Us</Link>);
|
|
11
|
+
|
|
12
|
+
const link = screen.getByRole("link");
|
|
13
|
+
expect(link).toBeInTheDocument();
|
|
14
|
+
expect(link).toHaveTextContent("About Us");
|
|
15
|
+
expect(link).toHaveAttribute("href", "/about");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should render as an anchor element", () => {
|
|
19
|
+
render(<Link href="/test">Link</Link>);
|
|
20
|
+
|
|
21
|
+
const link = screen.getByRole("link");
|
|
22
|
+
expect(link.tagName).toBe("A");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should render with custom classes via UI component", () => {
|
|
26
|
+
render(
|
|
27
|
+
<Link href="/" classes="custom-link-class">
|
|
28
|
+
Test
|
|
29
|
+
</Link>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const link = screen.getByRole("link");
|
|
33
|
+
expect(link).toHaveClass("custom-link-class");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should apply custom styles via styles prop", () => {
|
|
37
|
+
render(
|
|
38
|
+
<Link href="/" styles={{ color: "red" }}>
|
|
39
|
+
Styled Link
|
|
40
|
+
</Link>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const link = screen.getByRole("link");
|
|
44
|
+
const styleAttr = link.getAttribute("style") || "";
|
|
45
|
+
expect(styleAttr).toContain("color");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should render children correctly", () => {
|
|
49
|
+
render(
|
|
50
|
+
<Link href="/">
|
|
51
|
+
<span data-testid="child-element">Child Content</span>
|
|
52
|
+
</Link>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(screen.getByTestId("child-element")).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText("Child Content")).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Target Attribute", () => {
|
|
61
|
+
it("should render with target attribute", () => {
|
|
62
|
+
render(
|
|
63
|
+
<Link href="https://example.com" target="_blank">
|
|
64
|
+
External
|
|
65
|
+
</Link>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const link = screen.getByRole("link");
|
|
69
|
+
expect(link).toHaveAttribute("target", "_blank");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should work with different target values", () => {
|
|
73
|
+
const { rerender } = render(
|
|
74
|
+
<Link href="/" target="_self">
|
|
75
|
+
Self
|
|
76
|
+
</Link>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
let link = screen.getByRole("link");
|
|
80
|
+
expect(link).toHaveAttribute("target", "_self");
|
|
81
|
+
|
|
82
|
+
rerender(
|
|
83
|
+
<Link href="/" target="_parent">
|
|
84
|
+
Parent
|
|
85
|
+
</Link>
|
|
86
|
+
);
|
|
87
|
+
link = screen.getByRole("link");
|
|
88
|
+
expect(link).toHaveAttribute("target", "_parent");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Security (rel attribute)", () => {
|
|
93
|
+
it("should automatically add security attributes for target=_blank", () => {
|
|
94
|
+
render(
|
|
95
|
+
<Link href="https://example.com" target="_blank">
|
|
96
|
+
External Link
|
|
97
|
+
</Link>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const link = screen.getByRole("link");
|
|
101
|
+
const rel = link.getAttribute("rel");
|
|
102
|
+
|
|
103
|
+
expect(rel).toContain("noopener");
|
|
104
|
+
expect(rel).toContain("noreferrer");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should merge custom rel with security defaults for target=_blank", () => {
|
|
108
|
+
render(
|
|
109
|
+
<Link href="https://example.com" target="_blank" rel="nofollow author">
|
|
110
|
+
External Link
|
|
111
|
+
</Link>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const link = screen.getByRole("link");
|
|
115
|
+
const rel = link.getAttribute("rel");
|
|
116
|
+
|
|
117
|
+
// Should have security defaults
|
|
118
|
+
expect(rel).toContain("noopener");
|
|
119
|
+
expect(rel).toContain("noreferrer");
|
|
120
|
+
|
|
121
|
+
// Should also have custom values
|
|
122
|
+
expect(rel).toContain("nofollow");
|
|
123
|
+
expect(rel).toContain("author");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should use provided rel as-is when target is not _blank", () => {
|
|
127
|
+
render(
|
|
128
|
+
<Link href="/internal" rel="author">
|
|
129
|
+
Internal Link
|
|
130
|
+
</Link>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const link = screen.getByRole("link");
|
|
134
|
+
expect(link).toHaveAttribute("rel", "author");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should not add security attributes for internal links", () => {
|
|
138
|
+
render(<Link href="/about">About</Link>);
|
|
139
|
+
|
|
140
|
+
const link = screen.getByRole("link");
|
|
141
|
+
expect(link).not.toHaveAttribute("rel");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should handle empty rel attribute gracefully", () => {
|
|
145
|
+
render(
|
|
146
|
+
<Link href="https://example.com" target="_blank" rel="">
|
|
147
|
+
External
|
|
148
|
+
</Link>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const link = screen.getByRole("link");
|
|
152
|
+
const rel = link.getAttribute("rel");
|
|
153
|
+
|
|
154
|
+
expect(rel).toContain("noopener");
|
|
155
|
+
expect(rel).toContain("noreferrer");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should not duplicate rel tokens when merging", () => {
|
|
159
|
+
render(
|
|
160
|
+
<Link
|
|
161
|
+
href="https://example.com"
|
|
162
|
+
target="_blank"
|
|
163
|
+
rel="noopener nofollow"
|
|
164
|
+
>
|
|
165
|
+
External
|
|
166
|
+
</Link>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const link = screen.getByRole("link");
|
|
170
|
+
const rel = link.getAttribute("rel") || "";
|
|
171
|
+
const tokens = rel.split(/\s+/);
|
|
172
|
+
|
|
173
|
+
// Check for duplicates
|
|
174
|
+
const uniqueTokens = new Set(tokens);
|
|
175
|
+
expect(tokens.length).toBe(uniqueTokens.size);
|
|
176
|
+
|
|
177
|
+
// Should have all three: noopener, noreferrer, nofollow
|
|
178
|
+
expect(uniqueTokens.has("noopener")).toBe(true);
|
|
179
|
+
expect(uniqueTokens.has("noreferrer")).toBe(true);
|
|
180
|
+
expect(uniqueTokens.has("nofollow")).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("Prefetch", () => {
|
|
185
|
+
it("should add prefetch to rel when prefetch=true and target=_blank", () => {
|
|
186
|
+
render(
|
|
187
|
+
<Link href="https://example.com" target="_blank" prefetch>
|
|
188
|
+
Prefetch Link
|
|
189
|
+
</Link>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const link = screen.getByRole("link");
|
|
193
|
+
const rel = link.getAttribute("rel");
|
|
194
|
+
|
|
195
|
+
expect(rel).toContain("noopener");
|
|
196
|
+
expect(rel).toContain("noreferrer");
|
|
197
|
+
expect(rel).toContain("prefetch");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should not add prefetch when prefetch=false", () => {
|
|
201
|
+
render(
|
|
202
|
+
<Link href="https://example.com" target="_blank" prefetch={false}>
|
|
203
|
+
No Prefetch
|
|
204
|
+
</Link>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const link = screen.getByRole("link");
|
|
208
|
+
const rel = link.getAttribute("rel");
|
|
209
|
+
|
|
210
|
+
expect(rel).not.toContain("prefetch");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should not add prefetch for internal links", () => {
|
|
214
|
+
render(
|
|
215
|
+
<Link href="/internal" prefetch>
|
|
216
|
+
Internal
|
|
217
|
+
</Link>
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const link = screen.getByRole("link");
|
|
221
|
+
const rel = link.getAttribute("rel");
|
|
222
|
+
|
|
223
|
+
// Internal links don't get prefetch (only external with target="_blank")
|
|
224
|
+
expect(rel).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("Button Styling", () => {
|
|
229
|
+
it("should apply data-btn attribute when btnStyle is provided", () => {
|
|
230
|
+
render(
|
|
231
|
+
<Link href="/" btnStyle="primary">
|
|
232
|
+
Button Link
|
|
233
|
+
</Link>
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const link = screen.getByRole("link");
|
|
237
|
+
expect(link).toHaveAttribute("data-btn", "primary");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should render button-styled link with <b> wrapper", () => {
|
|
241
|
+
render(
|
|
242
|
+
<Link href="/">
|
|
243
|
+
<b>Button Text</b>
|
|
244
|
+
</Link>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const link = screen.getByRole("link");
|
|
248
|
+
const bold = link.querySelector("b");
|
|
249
|
+
|
|
250
|
+
expect(bold).toBeInTheDocument();
|
|
251
|
+
expect(bold).toHaveTextContent("Button Text");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should render pill-styled link with <i> wrapper", () => {
|
|
255
|
+
render(
|
|
256
|
+
<Link href="/">
|
|
257
|
+
<i>Pill Text</i>
|
|
258
|
+
</Link>
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const link = screen.getByRole("link");
|
|
262
|
+
const italic = link.querySelector("i");
|
|
263
|
+
|
|
264
|
+
expect(italic).toBeInTheDocument();
|
|
265
|
+
expect(italic).toHaveTextContent("Pill Text");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("Event Handlers", () => {
|
|
270
|
+
it("should call onClick when link is clicked", async () => {
|
|
271
|
+
const user = userEvent.setup();
|
|
272
|
+
const handleClick = vi.fn();
|
|
273
|
+
|
|
274
|
+
render(
|
|
275
|
+
<Link href="/test" onClick={handleClick}>
|
|
276
|
+
Click Me
|
|
277
|
+
</Link>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const link = screen.getByRole("link");
|
|
281
|
+
await user.click(link);
|
|
282
|
+
|
|
283
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(handleClick).toHaveBeenCalledWith(
|
|
285
|
+
expect.objectContaining({
|
|
286
|
+
type: "click",
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should call onClick when activated with keyboard (Enter)", async () => {
|
|
292
|
+
const user = userEvent.setup();
|
|
293
|
+
const handleClick = vi.fn();
|
|
294
|
+
|
|
295
|
+
render(
|
|
296
|
+
<Link href="/test" onClick={handleClick}>
|
|
297
|
+
Click Me
|
|
298
|
+
</Link>
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const link = screen.getByRole("link");
|
|
302
|
+
|
|
303
|
+
// Focus the link
|
|
304
|
+
link.focus();
|
|
305
|
+
expect(link).toHaveFocus();
|
|
306
|
+
|
|
307
|
+
// Press Enter to activate
|
|
308
|
+
await user.keyboard("{Enter}");
|
|
309
|
+
|
|
310
|
+
// onClick should be called for keyboard activation
|
|
311
|
+
expect(handleClick).toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should call onPointerDown when link is clicked", async () => {
|
|
315
|
+
const user = userEvent.setup();
|
|
316
|
+
const handlePointerDown = vi.fn();
|
|
317
|
+
|
|
318
|
+
render(
|
|
319
|
+
<Link href="/test" onPointerDown={handlePointerDown}>
|
|
320
|
+
Click Me
|
|
321
|
+
</Link>
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const link = screen.getByRole("link");
|
|
325
|
+
await user.click(link);
|
|
326
|
+
|
|
327
|
+
expect(handlePointerDown).toHaveBeenCalledTimes(1);
|
|
328
|
+
expect(handlePointerDown).toHaveBeenCalledWith(
|
|
329
|
+
expect.objectContaining({
|
|
330
|
+
type: "pointerdown",
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should call both onClick and onPointerDown when provided", async () => {
|
|
336
|
+
const user = userEvent.setup();
|
|
337
|
+
const handleClick = vi.fn();
|
|
338
|
+
const handlePointerDown = vi.fn();
|
|
339
|
+
|
|
340
|
+
render(
|
|
341
|
+
<Link href="/test" onClick={handleClick} onPointerDown={handlePointerDown}>
|
|
342
|
+
Click Me
|
|
343
|
+
</Link>
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const link = screen.getByRole("link");
|
|
347
|
+
await user.click(link);
|
|
348
|
+
|
|
349
|
+
// Both handlers should be called
|
|
350
|
+
expect(handleClick).toHaveBeenCalled();
|
|
351
|
+
expect(handlePointerDown).toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should NOT call onPointerDown on keyboard activation (Enter)", async () => {
|
|
355
|
+
const user = userEvent.setup();
|
|
356
|
+
const handlePointerDown = vi.fn();
|
|
357
|
+
|
|
358
|
+
render(
|
|
359
|
+
<Link href="/test" onPointerDown={handlePointerDown}>
|
|
360
|
+
Click Me
|
|
361
|
+
</Link>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const link = screen.getByRole("link");
|
|
365
|
+
link.focus();
|
|
366
|
+
|
|
367
|
+
// Press Enter
|
|
368
|
+
await user.keyboard("{Enter}");
|
|
369
|
+
|
|
370
|
+
// onPointerDown should NOT be called (only pointer events trigger it)
|
|
371
|
+
expect(handlePointerDown).not.toHaveBeenCalled();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should not throw error when onClick is not provided", async () => {
|
|
375
|
+
const user = userEvent.setup();
|
|
376
|
+
|
|
377
|
+
render(<Link href="/test">Click Me</Link>);
|
|
378
|
+
|
|
379
|
+
const link = screen.getByRole("link");
|
|
380
|
+
await expect(user.click(link)).resolves.not.toThrow();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should not throw error when onPointerDown is not provided", async () => {
|
|
384
|
+
const user = userEvent.setup();
|
|
385
|
+
|
|
386
|
+
render(<Link href="/test">Click Me</Link>);
|
|
387
|
+
|
|
388
|
+
const link = screen.getByRole("link");
|
|
389
|
+
await expect(user.click(link)).resolves.not.toThrow();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("Ref Forwarding", () => {
|
|
394
|
+
it("should forward ref to the anchor element", () => {
|
|
395
|
+
const ref = React.createRef<HTMLAnchorElement>();
|
|
396
|
+
|
|
397
|
+
render(
|
|
398
|
+
<Link ref={ref} href="/test">
|
|
399
|
+
Test Link
|
|
400
|
+
</Link>
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
|
|
404
|
+
expect(ref.current?.tagName).toBe("A");
|
|
405
|
+
expect(ref.current?.href).toContain("/test");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should allow programmatic focus via ref", () => {
|
|
409
|
+
const ref = React.createRef<HTMLAnchorElement>();
|
|
410
|
+
|
|
411
|
+
render(
|
|
412
|
+
<Link ref={ref} href="/test">
|
|
413
|
+
Focusable Link
|
|
414
|
+
</Link>
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
ref.current?.focus();
|
|
418
|
+
expect(ref.current).toHaveFocus();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("Accessibility", () => {
|
|
423
|
+
it("should render with proper role", () => {
|
|
424
|
+
render(<Link href="/test">Accessible Link</Link>);
|
|
425
|
+
|
|
426
|
+
const link = screen.getByRole("link");
|
|
427
|
+
expect(link).toBeInTheDocument();
|
|
428
|
+
expect(link).toBeVisible();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should render external links accessibly", () => {
|
|
432
|
+
render(
|
|
433
|
+
<Link href="https://example.com" target="_blank">
|
|
434
|
+
External Link
|
|
435
|
+
</Link>
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const link = screen.getByRole("link");
|
|
439
|
+
expect(link).toBeInTheDocument();
|
|
440
|
+
expect(link).toHaveAttribute("target", "_blank");
|
|
441
|
+
|
|
442
|
+
// Security attributes present
|
|
443
|
+
const rel = link.getAttribute("rel");
|
|
444
|
+
expect(rel).toContain("noopener");
|
|
445
|
+
expect(rel).toContain("noreferrer");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("should render button-styled links with semantic anchor", () => {
|
|
449
|
+
render(
|
|
450
|
+
<Link href="/action">
|
|
451
|
+
<b>Call to Action</b>
|
|
452
|
+
</Link>
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const link = screen.getByRole("link");
|
|
456
|
+
expect(link.tagName).toBe("A");
|
|
457
|
+
expect(link).toHaveAttribute("href", "/action");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should support aria-label for icon-only links", () => {
|
|
461
|
+
render(
|
|
462
|
+
<Link href="/settings" aria-label="Open settings">
|
|
463
|
+
<svg aria-hidden="true">
|
|
464
|
+
<path d="M0 0h24v24H0z" />
|
|
465
|
+
</svg>
|
|
466
|
+
</Link>
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const link = screen.getByRole("link");
|
|
470
|
+
expect(link).toHaveAccessibleName("Open settings");
|
|
471
|
+
|
|
472
|
+
// Verify SVG is hidden from screen readers
|
|
473
|
+
const svg = link.querySelector("svg");
|
|
474
|
+
expect(svg).toHaveAttribute("aria-hidden", "true");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should be keyboard accessible", async () => {
|
|
478
|
+
const user = userEvent.setup();
|
|
479
|
+
|
|
480
|
+
render(<Link href="/test">Keyboard Link</Link>);
|
|
481
|
+
|
|
482
|
+
const link = screen.getByRole("link");
|
|
483
|
+
|
|
484
|
+
// Tab to link
|
|
485
|
+
await user.tab();
|
|
486
|
+
expect(link).toHaveFocus();
|
|
487
|
+
|
|
488
|
+
// Press Enter should work (default browser behavior)
|
|
489
|
+
// We just verify focus worked
|
|
490
|
+
expect(link).toHaveFocus();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should have accessible name from text content", () => {
|
|
494
|
+
render(<Link href="/test">Read installation guide</Link>);
|
|
495
|
+
|
|
496
|
+
const link = screen.getByRole("link");
|
|
497
|
+
expect(link).toHaveAccessibleName("Read installation guide");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("should support aria-describedby for additional context", () => {
|
|
501
|
+
render(
|
|
502
|
+
<>
|
|
503
|
+
<span id="link-description">Opens in a new window</span>
|
|
504
|
+
<Link href="https://example.com" aria-describedby="link-description">
|
|
505
|
+
External Resource
|
|
506
|
+
</Link>
|
|
507
|
+
</>
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const link = screen.getByRole("link");
|
|
511
|
+
expect(link).toHaveAttribute("aria-describedby", "link-description");
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
describe("URL Schemes", () => {
|
|
516
|
+
it("should support mailto: links", () => {
|
|
517
|
+
render(<Link href="mailto:test@example.com">Email Us</Link>);
|
|
518
|
+
|
|
519
|
+
const link = screen.getByRole("link");
|
|
520
|
+
expect(link).toHaveAttribute("href", "mailto:test@example.com");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should support tel: links", () => {
|
|
524
|
+
render(<Link href="tel:+1234567890">Call Us</Link>);
|
|
525
|
+
|
|
526
|
+
const link = screen.getByRole("link");
|
|
527
|
+
expect(link).toHaveAttribute("href", "tel:+1234567890");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should support hash/anchor links", () => {
|
|
531
|
+
render(<Link href="#section-1">Jump to Section</Link>);
|
|
532
|
+
|
|
533
|
+
const link = screen.getByRole("link");
|
|
534
|
+
expect(link).toHaveAttribute("href", "#section-1");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("should support relative paths", () => {
|
|
538
|
+
render(<Link href="../parent">Go to Parent</Link>);
|
|
539
|
+
|
|
540
|
+
const link = screen.getByRole("link");
|
|
541
|
+
expect(link).toHaveAttribute("href", "../parent");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("should support absolute paths", () => {
|
|
545
|
+
render(<Link href="/absolute/path">Absolute Path</Link>);
|
|
546
|
+
|
|
547
|
+
const link = screen.getByRole("link");
|
|
548
|
+
expect(link).toHaveAttribute("href", "/absolute/path");
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
describe("Display Name", () => {
|
|
553
|
+
it("should have correct displayName for debugging", () => {
|
|
554
|
+
expect(Link.displayName).toBe("Link");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe("Props Spreading", () => {
|
|
559
|
+
it("should spread additional HTML attributes", () => {
|
|
560
|
+
render(
|
|
561
|
+
<Link href="/" data-testid="custom-link" id="link-123">
|
|
562
|
+
Test
|
|
563
|
+
</Link>
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const link = screen.getByTestId("custom-link");
|
|
567
|
+
expect(link).toHaveAttribute("id", "link-123");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("should support title attribute", () => {
|
|
571
|
+
render(
|
|
572
|
+
<Link href="/" title="Additional information">
|
|
573
|
+
Hover Me
|
|
574
|
+
</Link>
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const link = screen.getByRole("link");
|
|
578
|
+
expect(link).toHaveAttribute("title", "Additional information");
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe("Edge Cases", () => {
|
|
583
|
+
it("should handle missing href gracefully", () => {
|
|
584
|
+
// href is optional in the type, testing edge case behavior
|
|
585
|
+
render(<Link>No href</Link>);
|
|
586
|
+
|
|
587
|
+
// Should still render, though not a valid link
|
|
588
|
+
const element = screen.getByText("No href");
|
|
589
|
+
expect(element).toBeInTheDocument();
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("should handle whitespace-only rel values", () => {
|
|
593
|
+
render(
|
|
594
|
+
<Link href="https://example.com" target="_blank" rel=" ">
|
|
595
|
+
External
|
|
596
|
+
</Link>
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const link = screen.getByRole("link");
|
|
600
|
+
const rel = link.getAttribute("rel");
|
|
601
|
+
|
|
602
|
+
// Should still include security tokens
|
|
603
|
+
expect(rel).toContain("noopener");
|
|
604
|
+
expect(rel).toContain("noreferrer");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("should handle multiple whitespace between rel tokens", () => {
|
|
608
|
+
render(
|
|
609
|
+
<Link
|
|
610
|
+
href="https://example.com"
|
|
611
|
+
target="_blank"
|
|
612
|
+
rel="nofollow author"
|
|
613
|
+
>
|
|
614
|
+
External
|
|
615
|
+
</Link>
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const link = screen.getByRole("link");
|
|
619
|
+
const rel = link.getAttribute("rel") || "";
|
|
620
|
+
|
|
621
|
+
// Split should handle multiple spaces
|
|
622
|
+
expect(rel).toContain("noopener");
|
|
623
|
+
expect(rel).toContain("noreferrer");
|
|
624
|
+
expect(rel).toContain("nofollow");
|
|
625
|
+
expect(rel).toContain("author");
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe("Performance", () => {
|
|
630
|
+
it("should memoize rel computation to avoid unnecessary recalculations", () => {
|
|
631
|
+
const { rerender } = render(
|
|
632
|
+
<Link href="https://example.com" target="_blank">
|
|
633
|
+
Link
|
|
634
|
+
</Link>
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const link1 = screen.getByRole("link");
|
|
638
|
+
const rel1 = link1.getAttribute("rel");
|
|
639
|
+
|
|
640
|
+
// Rerender with same props
|
|
641
|
+
rerender(
|
|
642
|
+
<Link href="https://example.com" target="_blank">
|
|
643
|
+
Link
|
|
644
|
+
</Link>
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const link2 = screen.getByRole("link");
|
|
648
|
+
const rel2 = link2.getAttribute("rel");
|
|
649
|
+
|
|
650
|
+
// Should produce same result
|
|
651
|
+
expect(rel1).toBe(rel2);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("should update rel when dependencies change", () => {
|
|
655
|
+
const { rerender } = render(
|
|
656
|
+
<Link href="https://example.com" target="_blank" prefetch={false}>
|
|
657
|
+
Link
|
|
658
|
+
</Link>
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const link1 = screen.getByRole("link");
|
|
662
|
+
const rel1 = link1.getAttribute("rel");
|
|
663
|
+
expect(rel1).not.toContain("prefetch");
|
|
664
|
+
|
|
665
|
+
// Rerender with prefetch=true
|
|
666
|
+
rerender(
|
|
667
|
+
<Link href="https://example.com" target="_blank" prefetch={true}>
|
|
668
|
+
Link
|
|
669
|
+
</Link>
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const link2 = screen.getByRole("link");
|
|
673
|
+
const rel2 = link2.getAttribute("rel");
|
|
674
|
+
expect(rel2).toContain("prefetch");
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
});
|