1/* oxlint-disable t3code/no-unscoped-has -- the fixtures are invalid on purpose */
2import { assert, describe } from "@effect/vitest";
3
4import { createOxlintRuleHarness } from "../test/utils.ts";
5
6const rule = createOxlintRuleHarness("t3code/no-unscoped-has", {
7 filename: "fixture.tsx",
8});
9
10describe("t3code/no-unscoped-has", () => {
11 rule.valid(
12 "allows :has() on the element itself",
13 `const className = "[&:has([data-slot=icon])]:ps-2";`,
14 );
15
16 rule.valid(
17 "allows :has() anchored to an attribute",
18 `const className = "[&+[data-chat-composer-form]:has(>[data-slot=banner])]:mt-0";`,
19 );
20
21 rule.valid(
22 "allows :has() inside :not() on the element itself",
23 `const className = "[&:not(:has(+[data-slot=footer]))]:rounded-b-2xl";`,
24 );
25
26 rule.valid(
27 "allows built-in has-* variants",
28 `const className = "has-[>[data-slot=icon]]:ps-2 group-has-[:checked]:opacity-100";`,
29 );
30
31 rule.valid(
32 "allows a sibling selector without :has()",
33 `const className = "[&+*_[data-chat-composer-form]>[data-slot=attachment]]:before:rounded-none";`,
34 );
35
36 rule.valid("ignores prose mentioning :has()", `const note = "uses :has( for styling";`);
37
38 rule.valid(
39 "ignores :has() inside an arbitrary value",
40 `const className = "before:content-[':has(foo)']";`,
41 );
42
43 rule.valid(
44 "allows a negated :has() on the element itself",
45 `const className = "[&:not(.collapsed):not(:has(>[data-slot=icon]))]:ps-2";`,
46 );
47
48 rule.valid(
49 "allows :has() on a group or peer element",
50 `const className = "group-[:has(input)]:p-2 peer-[:has(input)]:p-2 group-[&:has(input)]/row:p-2";`,
51 );
52
53 rule.valid(
54 "allows a selector list whose own branch is anchored",
55 `const className = "[:is(.a,.b):has(x)_&]:p-2 [&:not(.a,:has(x))]:p-2";`,
56 );
57
58 rule.valid(
59 "ignores :has() text in quoted attribute values",
60 `const className = "data-[foo='_:has(x)']:p-2 [&_[data-query='_:has(foo)']]:p-2";`,
61 );
62
63 rule.valid(
64 "allows a selector without & on the element itself",
65 `const className = "[:has(>input)]:p-2 not-[:has(>[data-slot=icon])]:ps-2";`,
66 );
67
68 rule.invalid(
69 "reports a sibling :has() with nothing anchoring it",
70 `const className = "[&+:has([data-chat-composer-form])_[data-chat-composer-form]]:before:rounded-none";`,
71 (output) => {
72 assert.match(output, /Anchor the :has\(\)/);
73 },
74 );
75
76 rule.invalid(
77 "reports a descendant :has() with nothing anchoring it",
78 `const className = cn("p-2", "[&_:has(>input)]:gap-1");`,
79 );
80
81 rule.invalid(
82 "reports a universal :has() ancestor",
83 "const className = `flex [*:has([data-open])_&]:hidden`;",
84 );
85
86 rule.invalid(
87 "reports :has() whose only anchor is negated",
88 `const className = "[*:not(.safe):has([data-open])_&]:hidden";`,
89 );
90
91 rule.invalid("reports uppercase :HAS()", `const className = "[*:HAS([data-open])_&]:hidden";`);
92
93 rule.invalid(
94 "reports a selector-list branch borrowing another branch's anchor",
95 `const className = "[.safe,:has(input)_&]:p-2";`,
96 );
97
98 rule.invalid(
99 "reports an unanchored branch inside :is()",
100 `const className = "[&_:is(.safe,:has(input))]:p-2";`,
101 );
102
103 rule.invalid(
104 "reports an unanchored :has() inside a named group variant",
105 `const className = "group-[&_:has(x)]/row:p-2";`,
106 );
107
108 rule.invalid(
109 "reports a :has() on an ancestor of a selector without &",
110 `const className = "[:has(x)_.foo]:p-2";`,
111 );
112
113 rule.invalid(
114 "reports a :has() on an ancestor of a group",
115 `const className = "group-[:has(x)_.y]:p-2";`,
116 );
117
118 rule.invalid("reports an in-* ancestor :has()", `const className = "in-[:has(x)]:p-2";`);
119
120 rule.invalid(
121 "reports :has() anchored to the document root",
122 `const className = "[body:has([data-dialog-open])_&]:overflow-hidden";`,
123 );
124});
125
126const indicators = createOxlintRuleHarness("t3code/no-outset-state-indicators", {
127 filename: "fixture.tsx",
128});
129
130describe("t3code/no-outset-state-indicators", () => {
131 describe.each(
132 Object.entries({
133 "inset state rings": `const el = <button className="focus-visible:ring-2 focus-visible:ring-inset ring-1 ring-border" />;`,
134 "an unconditional inset": `const el = <button className="ring-inset data-selected:ring-2" />;`,
135 "an inset in a sibling composer argument": `const value = cn(active && "dark:focus-visible:ring-2", "focus-visible:ring-inset");`,
136 "an inset in a nested conditional composer": `const value = cn(enabled && cn("focus-visible:ring-2", "focus-visible:ring-inset"));`,
137 "outlines covered by the inward default": `const el = <button className="focus-visible:outline-2 focus-visible:outline-ring" />;`,
138 "wider outlines with a matching inward offset": `const el = <button className="focus-visible:outline-3 focus-visible:-outline-offset-3" />;`,
139 "arbitrary pixel outlines with a matching inward offset": `const el = <button className="focus-visible:outline-[3px] focus-visible:-outline-offset-3" />;`,
140 "arbitrary outline colors": `const el = <button className="focus-visible:outline-[#243c5a]" />;`,
141 "decorative rings without a state": `const el = <img className="ring-2 ring-background" />;`,
142 prose: `const note = "the focus ring stays visible";`,
143 }),
144 )("valid: %s", (name, source) => {
145 indicators.valid(name, source);
146 });
147
148 describe.each(
149 Object.entries({
150 "state rings without an inset": `const el = <button className="focus-visible:ring-2 focus-visible:ring-ring" />;`,
151 "an inset from another state": `const el = <button className="focus:ring-inset focus-visible:ring-2" />;`,
152 "an inset from another conditional branch": `const value = cn(active ? "focus:ring-2" : "ring-inset");`,
153 "an inset passed to another function": `const value = cn("focus-visible:ring-2", label("ring-inset"));`,
154 "arbitrary state ring widths": `const value = cn("has-[:focus-visible]:ring-[3px]");`,
155 "positive outline offsets": `const el = <button className="focus-visible:outline-2 focus-visible:outline-offset-2" />;`,
156 "arbitrary outline offsets": `const el = <button className="focus-visible:[outline-offset:2px]" />;`,
157 "wide outlines without an inward offset":
158 "const className = `focus-visible:outline-4 ${layout}`;",
159 }),
160 )("invalid: %s", (name, source) => {
161 indicators.invalid(name, source);
162 });
163});