srcdev-nuxt-components 9.6.13 → 9.6.15
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/.claude/rules/consumer/story-register-components.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/display-qr-code.md +22 -4
- package/.claude/skills/components/eyebrow-text.md +9 -5
- package/.claude/skills/components/header-block.md +13 -5
- package/.claude/skills/components/hero-text.md +13 -9
- package/.claude/skills/components/link-text.md +8 -5
- package/.claude/skills/components/pop-over.md +17 -3
- package/.claude/skills/components/sliding-toggle.md +108 -0
- package/.claude/skills/components/tabbed-content.md +8 -3
- package/.claude/skills/composable-sliding-indicator.md +60 -0
- package/.claude/skills/index.md +2 -0
- package/.claude/skills/storybook-add-story.md +22 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- package/.claude/skills/testing-add-unit-test.md +25 -0
- package/.claude/skills/theming-component-token-pattern.md +18 -0
- package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
- package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
- package/.vscode/srcdev-component-link-text.code-snippets +3 -3
- package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
- package/.vscode/srcdev-component-sliding-toggle.code-snippets +37 -0
- package/.vscode/srcdev-component-tabbed-content.code-snippets +1 -1
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
- package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
- package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +15 -2
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +4 -4
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +4 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
- package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
- package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
- package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
- package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
- package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
- package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
- package/app/components/02.molecules/sliding-toggle/CONSUMER-STYLING.md +109 -0
- package/app/components/02.molecules/sliding-toggle/SlidingToggle.vue +307 -0
- package/app/components/02.molecules/sliding-toggle/stories/SlidingToggle.stories.ts +357 -0
- package/app/components/02.molecules/sliding-toggle/tests/SlidingToggle.spec.ts +335 -0
- package/app/components/02.molecules/sliding-toggle/tests/__snapshots__/SlidingToggle.spec.ts.snap +3 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
- package/app/composables/tests/useSlidingIndicator.spec.ts +143 -0
- package/app/composables/useSlidingIndicator.ts +56 -0
- package/app/composables/useTabs.ts +4 -31
- package/app/types/components/index.ts +1 -0
- package/app/types/components/sliding-toggle.d.ts +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
import SlidingToggle from "../SlidingToggle.vue";
|
|
5
|
+
import type { SlidingToggleOption } from "~/types/components";
|
|
6
|
+
|
|
7
|
+
const billing: SlidingToggleOption[] = [
|
|
8
|
+
{ value: "monthly", label: "Monthly" },
|
|
9
|
+
{ value: "yearly", label: "Yearly" },
|
|
10
|
+
{ value: "lifetime", label: "Lifetime" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const observed: Element[] = [];
|
|
14
|
+
|
|
15
|
+
class MockResizeObserver {
|
|
16
|
+
observe = vi.fn((element: Element) => observed.push(element));
|
|
17
|
+
unobserve = vi.fn();
|
|
18
|
+
disconnect = vi.fn(() => observed.splice(0));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
22
|
+
|
|
23
|
+
const mountToggle = async (props: Record<string, unknown> = {}, attrs: Record<string, unknown> = {}) => {
|
|
24
|
+
const holder: { current?: Awaited<ReturnType<typeof mountSuspended>> } = {};
|
|
25
|
+
const onUpdate = (value: string | number) => {
|
|
26
|
+
void holder.current?.setProps({ modelValue: value });
|
|
27
|
+
};
|
|
28
|
+
const wrapper = await mountSuspended(SlidingToggle, {
|
|
29
|
+
props: { options: billing, modelValue: "monthly", ariaLabel: "Billing period", "onUpdate:modelValue": onUpdate, ...props },
|
|
30
|
+
attrs,
|
|
31
|
+
attachTo: document.body,
|
|
32
|
+
});
|
|
33
|
+
holder.current = wrapper;
|
|
34
|
+
mounted.push(wrapper);
|
|
35
|
+
return wrapper;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const radios = (wrapper: Awaited<ReturnType<typeof mountToggle>>) => wrapper.findAll('[role="radio"]');
|
|
39
|
+
|
|
40
|
+
const mockRow = () => {
|
|
41
|
+
vi.spyOn(HTMLElement.prototype, "offsetLeft", "get").mockImplementation(function (this: HTMLElement) {
|
|
42
|
+
return this.dataset.index === undefined ? 0 : Number(this.dataset.index) * 100;
|
|
43
|
+
});
|
|
44
|
+
vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(function (this: HTMLElement) {
|
|
45
|
+
return this.dataset.index === undefined ? 0 : 100;
|
|
46
|
+
});
|
|
47
|
+
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockImplementation(function (this: HTMLElement) {
|
|
48
|
+
return this.classList.contains("sliding-toggle") ? 300 : 0;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const edge = (wrapper: Awaited<ReturnType<typeof mountToggle>>, name: string) =>
|
|
53
|
+
(wrapper.element as HTMLElement).style.getPropertyValue(`--_sliding-toggle-${name}`);
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.stubGlobal("ResizeObserver", MockResizeObserver);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
observed.splice(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("SlidingToggle", () => {
|
|
66
|
+
it("mounts without error", async () => {
|
|
67
|
+
const wrapper = await mountToggle();
|
|
68
|
+
expect(wrapper.vm).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders correct HTML structure", async () => {
|
|
72
|
+
const wrapper = await mountToggle({ styleClassPassthrough: "custom-class" });
|
|
73
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── Semantics ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
it("renders a named radiogroup", async () => {
|
|
79
|
+
const wrapper = await mountToggle();
|
|
80
|
+
expect(wrapper.attributes("role")).toBe("radiogroup");
|
|
81
|
+
expect(wrapper.attributes("aria-label")).toBe("Billing period");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("can be named by aria-labelledby instead", async () => {
|
|
85
|
+
const wrapper = await mountToggle({ ariaLabel: "", ariaLabelledby: "billing-heading" });
|
|
86
|
+
expect(wrapper.attributes("aria-label")).toBeUndefined();
|
|
87
|
+
expect(wrapper.attributes("aria-labelledby")).toBe("billing-heading");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders one type=button radio per option, labelled by its text", async () => {
|
|
91
|
+
const wrapper = await mountToggle();
|
|
92
|
+
const options = radios(wrapper);
|
|
93
|
+
expect(options).toHaveLength(3);
|
|
94
|
+
options.forEach((option, index) => {
|
|
95
|
+
expect(option.element.tagName).toBe("BUTTON");
|
|
96
|
+
expect(option.attributes("type")).toBe("button");
|
|
97
|
+
expect(option.text()).toBe(billing[index]!.label);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("marks the selected option aria-checked", async () => {
|
|
102
|
+
const wrapper = await mountToggle({ modelValue: "yearly" });
|
|
103
|
+
expect(radios(wrapper).map((option) => option.attributes("aria-checked"))).toEqual(["false", "true", "false"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("hides both indicators from assistive tech", async () => {
|
|
107
|
+
const wrapper = await mountToggle();
|
|
108
|
+
wrapper.findAll(".sliding-toggle-indicator").forEach((indicator) => {
|
|
109
|
+
expect(indicator.attributes("aria-hidden")).toBe("true");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── Selection ───────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
it("selects on click", async () => {
|
|
116
|
+
const wrapper = await mountToggle();
|
|
117
|
+
await radios(wrapper)[2]!.trigger("click");
|
|
118
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["lifetime"]);
|
|
119
|
+
expect(radios(wrapper)[2]!.attributes("aria-checked")).toBe("true");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("follows an external v-model change", async () => {
|
|
123
|
+
const wrapper = await mountToggle();
|
|
124
|
+
await wrapper.setProps({ modelValue: "lifetime" });
|
|
125
|
+
expect(radios(wrapper)[2]!.attributes("aria-checked")).toBe("true");
|
|
126
|
+
expect(radios(wrapper)[0]!.attributes("aria-checked")).toBe("false");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("supports numeric values", async () => {
|
|
130
|
+
const wrapper = await mountToggle({
|
|
131
|
+
options: [
|
|
132
|
+
{ value: 1, label: "One" },
|
|
133
|
+
{ value: 2, label: "Two" },
|
|
134
|
+
],
|
|
135
|
+
modelValue: 1,
|
|
136
|
+
});
|
|
137
|
+
await radios(wrapper)[1]!.trigger("click");
|
|
138
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual([2]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── Roving tabindex ─────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
it("gives only the checked option the tab stop", async () => {
|
|
144
|
+
const wrapper = await mountToggle({ modelValue: "yearly" });
|
|
145
|
+
expect(radios(wrapper).map((option) => option.attributes("tabindex"))).toEqual(["-1", "0", "-1"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("gives the first enabled option the tab stop when the value matches no option", async () => {
|
|
149
|
+
const wrapper = await mountToggle({
|
|
150
|
+
modelValue: "weekly",
|
|
151
|
+
options: [{ value: "a", label: "A", disabled: true }, ...billing],
|
|
152
|
+
});
|
|
153
|
+
expect(radios(wrapper).map((option) => option.attributes("tabindex"))).toEqual(["-1", "0", "-1", "-1"]);
|
|
154
|
+
expect(wrapper.attributes("data-has-selection")).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ─── Keyboard ────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
it("ArrowRight selects and focuses the next option", async () => {
|
|
160
|
+
const wrapper = await mountToggle();
|
|
161
|
+
await radios(wrapper)[0]!.trigger("keydown", { key: "ArrowRight" });
|
|
162
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["yearly"]);
|
|
163
|
+
expect(document.activeElement).toBe(radios(wrapper)[1]!.element);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("ArrowDown also moves to the next option, and ArrowLeft/ArrowUp to the previous", async () => {
|
|
167
|
+
const wrapper = await mountToggle({ modelValue: "yearly" });
|
|
168
|
+
await radios(wrapper)[1]!.trigger("keydown", { key: "ArrowDown" });
|
|
169
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["lifetime"]);
|
|
170
|
+
await radios(wrapper)[2]!.trigger("keydown", { key: "ArrowUp" });
|
|
171
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["yearly"]);
|
|
172
|
+
await radios(wrapper)[1]!.trigger("keydown", { key: "ArrowLeft" });
|
|
173
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["monthly"]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("wraps from the last option to the first and back", async () => {
|
|
177
|
+
const wrapper = await mountToggle({ modelValue: "lifetime" });
|
|
178
|
+
await radios(wrapper)[2]!.trigger("keydown", { key: "ArrowRight" });
|
|
179
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["monthly"]);
|
|
180
|
+
await radios(wrapper)[0]!.trigger("keydown", { key: "ArrowLeft" });
|
|
181
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["lifetime"]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("Home and End select the first and last options", async () => {
|
|
185
|
+
const wrapper = await mountToggle({ modelValue: "yearly" });
|
|
186
|
+
await radios(wrapper)[1]!.trigger("keydown", { key: "End" });
|
|
187
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["lifetime"]);
|
|
188
|
+
await radios(wrapper)[2]!.trigger("keydown", { key: "Home" });
|
|
189
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["monthly"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("skips disabled options", async () => {
|
|
193
|
+
const wrapper = await mountToggle({
|
|
194
|
+
options: [billing[0]!, { ...billing[1]!, disabled: true }, billing[2]!],
|
|
195
|
+
});
|
|
196
|
+
await radios(wrapper)[0]!.trigger("keydown", { key: "ArrowRight" });
|
|
197
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["lifetime"]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("RTL swaps the arrow directions", async () => {
|
|
201
|
+
const wrapper = await mountToggle({}, { dir: "rtl" });
|
|
202
|
+
await radios(wrapper)[0]!.trigger("keydown", { key: "ArrowLeft" });
|
|
203
|
+
expect(wrapper.emitted("update:modelValue")?.at(-1)).toEqual(["yearly"]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("ignores other keys", async () => {
|
|
207
|
+
const wrapper = await mountToggle();
|
|
208
|
+
await radios(wrapper)[0]!.trigger("keydown", { key: "a" });
|
|
209
|
+
expect(wrapper.emitted("update:modelValue")).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ─── Disabled ────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
it("disables every option and blocks selection when the group is disabled", async () => {
|
|
215
|
+
const wrapper = await mountToggle({ disabled: true });
|
|
216
|
+
expect(wrapper.attributes("aria-disabled")).toBe("true");
|
|
217
|
+
radios(wrapper).forEach((option) => expect(option.attributes("disabled")).toBeDefined());
|
|
218
|
+
await radios(wrapper)[1]!.trigger("keydown", { key: "ArrowRight" });
|
|
219
|
+
expect(wrapper.emitted("update:modelValue")).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("does not select a disabled option on click", async () => {
|
|
223
|
+
const wrapper = await mountToggle({ options: [billing[0]!, { ...billing[1]!, disabled: true }, billing[2]!] });
|
|
224
|
+
const vm = wrapper.vm as unknown as { select: (index: number) => void };
|
|
225
|
+
vm.select(1);
|
|
226
|
+
await nextTick();
|
|
227
|
+
expect(wrapper.emitted("update:modelValue")).toBeUndefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─── Indicators ──────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
it("sets data-ready once mounted and measured", async () => {
|
|
233
|
+
const wrapper = await mountToggle();
|
|
234
|
+
expect(wrapper.attributes("data-ready")).toBeDefined();
|
|
235
|
+
expect(wrapper.attributes("data-has-selection")).toBeDefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("snaps the active indicator onto the checked option on mount", async () => {
|
|
239
|
+
mockRow();
|
|
240
|
+
const wrapper = await mountToggle({ modelValue: "yearly" });
|
|
241
|
+
expect(edge(wrapper, "active-start")).toBe("100px");
|
|
242
|
+
expect(edge(wrapper, "active-end")).toBe("100px");
|
|
243
|
+
expect(edge(wrapper, "active-duration")).toBe("0ms");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("slides the active indicator with the leading edge first", async () => {
|
|
247
|
+
mockRow();
|
|
248
|
+
const wrapper = await mountToggle();
|
|
249
|
+
await radios(wrapper)[2]!.trigger("click");
|
|
250
|
+
await nextTick();
|
|
251
|
+
expect(edge(wrapper, "active-start")).toBe("200px");
|
|
252
|
+
expect(edge(wrapper, "active-start-delay")).toBe("200ms");
|
|
253
|
+
expect(edge(wrapper, "active-end-delay")).toBe("0ms");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("moves the hover indicator with the pointer and back to the checked option on leave", async () => {
|
|
257
|
+
mockRow();
|
|
258
|
+
const wrapper = await mountToggle();
|
|
259
|
+
await radios(wrapper)[2]!.trigger("mouseenter");
|
|
260
|
+
expect(edge(wrapper, "hover-start")).toBe("200px");
|
|
261
|
+
expect(wrapper.attributes("data-hovering")).toBeDefined();
|
|
262
|
+
await wrapper.trigger("mouseleave");
|
|
263
|
+
expect(edge(wrapper, "hover-start")).toBe("0px");
|
|
264
|
+
expect(wrapper.attributes("data-hovering")).toBeUndefined();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("does not track hover when trackHover is false", async () => {
|
|
268
|
+
mockRow();
|
|
269
|
+
const wrapper = await mountToggle({ trackHover: false });
|
|
270
|
+
await radios(wrapper)[2]!.trigger("mouseenter");
|
|
271
|
+
expect(edge(wrapper, "hover-start")).toBe("0px");
|
|
272
|
+
expect(wrapper.attributes("data-hovering")).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("uses transitionDuration for the slide", async () => {
|
|
276
|
+
mockRow();
|
|
277
|
+
const wrapper = await mountToggle({ transitionDuration: 350 });
|
|
278
|
+
await radios(wrapper)[1]!.trigger("click");
|
|
279
|
+
await nextTick();
|
|
280
|
+
expect(edge(wrapper, "active-duration")).toBe("350ms");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ─── Re-measuring ────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
it("observes the track and each option for size changes", async () => {
|
|
286
|
+
const wrapper = await mountToggle();
|
|
287
|
+
expect(observed).toContain(wrapper.element);
|
|
288
|
+
radios(wrapper).forEach((option) => expect(observed).toContain(option.element));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("re-places the indicators when the options change", async () => {
|
|
292
|
+
mockRow();
|
|
293
|
+
const wrapper = await mountToggle({ modelValue: "lifetime" });
|
|
294
|
+
await wrapper.setProps({ options: [billing[2]!, billing[0]!] });
|
|
295
|
+
await nextTick();
|
|
296
|
+
await nextTick();
|
|
297
|
+
expect(edge(wrapper, "active-start")).toBe("0px");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ─── Worst-case data ─────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
it("renders an empty group without errors", async () => {
|
|
303
|
+
const wrapper = await mountToggle({ options: [] });
|
|
304
|
+
expect(radios(wrapper)).toHaveLength(0);
|
|
305
|
+
expect(wrapper.attributes("data-has-selection")).toBeUndefined();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("renders duplicate labels as separate radios", async () => {
|
|
309
|
+
const wrapper = await mountToggle({
|
|
310
|
+
options: [
|
|
311
|
+
{ value: "a", label: "Same" },
|
|
312
|
+
{ value: "b", label: "Same" },
|
|
313
|
+
],
|
|
314
|
+
modelValue: "b",
|
|
315
|
+
});
|
|
316
|
+
expect(radios(wrapper)).toHaveLength(2);
|
|
317
|
+
expect(radios(wrapper)[1]!.attributes("aria-checked")).toBe("true");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("renders HTML-like labels as text", async () => {
|
|
321
|
+
const wrapper = await mountToggle({ options: [{ value: "x", label: "<b>not bold</b>" }], modelValue: "x" });
|
|
322
|
+
expect(wrapper.find("b").exists()).toBe(false);
|
|
323
|
+
expect(radios(wrapper)[0]!.text()).toBe("<b>not bold</b>");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
329
|
+
const wrapper = await mountToggle({ styleClassPassthrough: ["original"] });
|
|
330
|
+
expect(wrapper.classes()).toContain("sliding-toggle");
|
|
331
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
332
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
333
|
+
expect(wrapper.classes()).toContain("updated");
|
|
334
|
+
});
|
|
335
|
+
});
|
package/app/components/02.molecules/sliding-toggle/tests/__snapshots__/SlidingToggle.spec.ts.snap
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SlidingToggle > renders correct HTML structure 1`] = `"<div class="sliding-toggle custom-class" role="radiogroup" aria-label="Billing period" data-has-selection="" style="--_sliding-toggle-active-duration: 0ms; --_sliding-toggle-active-start-delay: 0ms; --_sliding-toggle-active-end-delay: 0ms; --_sliding-toggle-active-start: 0px; --_sliding-toggle-active-end: 0px; --_sliding-toggle-hover-duration: 0ms; --_sliding-toggle-hover-start-delay: 0ms; --_sliding-toggle-hover-end-delay: 0ms; --_sliding-toggle-hover-start: 0px; --_sliding-toggle-hover-end: 0px;" data-ready=""><span class="sliding-toggle-indicator sliding-toggle-indicator-hover" aria-hidden="true"></span><span class="sliding-toggle-indicator sliding-toggle-indicator-active" aria-hidden="true"></span><button type="button" role="radio" class="sliding-toggle-option" data-index="0" aria-checked="true" tabindex="0">Monthly</button><button type="button" role="radio" class="sliding-toggle-option" data-index="1" aria-checked="false" tabindex="-1">Yearly</button><button type="button" role="radio" class="sliding-toggle-option" data-index="2" aria-checked="false" tabindex="-1">Lifetime</button></div>"`;
|
|
@@ -17,9 +17,9 @@ exports[`ServiceDetail > renders correct HTML structure 1`] = `
|
|
|
17
17
|
</li>
|
|
18
18
|
</ol>
|
|
19
19
|
</nav>
|
|
20
|
-
<div class="eyebrow-text large">Freehand colour artistry</div>
|
|
21
|
-
<h1 id="v-0-0" class="hero-text display axis
|
|
22
|
-
<!--v-if--><span class="text-
|
|
20
|
+
<div class="eyebrow-text" data-font-size="large">Freehand colour artistry</div>
|
|
21
|
+
<h1 id="v-0-0" class="hero-text" data-font-size="display" data-axis="horizontal">
|
|
22
|
+
<!--v-if--><span class="hero-text-segment normal">Balayage</span>
|
|
23
23
|
</h1>
|
|
24
24
|
<div class="service-detail__hero-pills"><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">2.5 - 3.5 hours</span></span><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">From £95</span></span></div>
|
|
25
25
|
</div>
|
|
@@ -30,12 +30,12 @@ exports[`ServiceDetail > renders correct HTML structure 1`] = `
|
|
|
30
30
|
<div class="service-detail__body">
|
|
31
31
|
<div class="service-detail__main">
|
|
32
32
|
<p class="page-body-normal">Balayage is a freehand hair colouring technique that creates a soft gradient.</p>
|
|
33
|
-
<h2 class="hero-text mb-20 subheading axis
|
|
34
|
-
<!--v-if--><span class="text-
|
|
33
|
+
<h2 class="hero-text mb-20" data-font-size="subheading" data-axis="horizontal">
|
|
34
|
+
<!--v-if--><span class="hero-text-segment normal">What is Balayage?</span>
|
|
35
35
|
</h2>
|
|
36
36
|
<p class="page-body-normal">The word balayage comes from the French for 'to sweep'.</p>
|
|
37
|
-
<h2 class="hero-text mb-20 subheading axis
|
|
38
|
-
<!--v-if--><span class="text-
|
|
37
|
+
<h2 class="hero-text mb-20" data-font-size="subheading" data-axis="horizontal">
|
|
38
|
+
<!--v-if--><span class="hero-text-segment normal">The Process</span>
|
|
39
39
|
</h2>
|
|
40
40
|
<ol role="list" class="stepper-list has-connectors service-detail__process">
|
|
41
41
|
<li class="stepper-list-item indicator-top indicator-circle" style="--_connector-left: 0px; --_connector-top: 0px; --_connector-height: 0px;">
|
|
@@ -57,8 +57,8 @@ exports[`ServiceDetail > renders correct HTML structure 1`] = `
|
|
|
57
57
|
</div>
|
|
58
58
|
</li>
|
|
59
59
|
</ol>
|
|
60
|
-
<h2 class="hero-text mb-20 subheading axis
|
|
61
|
-
<!--v-if--><span class="text-
|
|
60
|
+
<h2 class="hero-text mb-20" data-font-size="subheading" data-axis="horizontal">
|
|
61
|
+
<!--v-if--><span class="hero-text-segment normal">Ideal For</span>
|
|
62
62
|
</h2>
|
|
63
63
|
<ul class="service-detail__ideal-for">
|
|
64
64
|
<li class="service-detail__ideal-for-item"><span class="iconify i-mdi:diamond-stone service-detail__ideal-for-icon" aria-hidden="true"></span>
|
|
@@ -68,12 +68,12 @@ exports[`ServiceDetail > renders correct HTML structure 1`] = `
|
|
|
68
68
|
<p class="page-body-normal">Low-maintenance colour</p>
|
|
69
69
|
</li>
|
|
70
70
|
</ul>
|
|
71
|
-
<h2 class="hero-text mb-20 subheading axis
|
|
72
|
-
<!--v-if--><span class="text-
|
|
71
|
+
<h2 class="hero-text mb-20" data-font-size="subheading" data-axis="horizontal">
|
|
72
|
+
<!--v-if--><span class="hero-text-segment normal">Aftercare & Maintenance</span>
|
|
73
73
|
</h2>
|
|
74
74
|
<p class="page-body-normal">Balayage is low maintenance since regrowth is soft and natural.</p>
|
|
75
|
-
<h2 class="hero-text mb-20 subheading axis
|
|
76
|
-
<!--v-if--><span class="text-
|
|
75
|
+
<h2 class="hero-text mb-20" data-font-size="subheading" data-axis="horizontal">
|
|
76
|
+
<!--v-if--><span class="hero-text-segment normal">Frequently Asked Questions</span>
|
|
77
77
|
</h2>
|
|
78
78
|
<div class="service-detail__faqs">
|
|
79
79
|
<div class="service-detail__faq">
|
|
@@ -98,8 +98,8 @@ exports[`ServiceDetail > renders correct HTML structure 1`] = `
|
|
|
98
98
|
<div class="page-row content service-detail__final-cta-row">
|
|
99
99
|
<div class="service-detail__final-cta">
|
|
100
100
|
<div class="service-detail__final-cta-copy">
|
|
101
|
-
<h2 class="hero-text subheading axis
|
|
102
|
-
<!--v-if--><span class="text-
|
|
101
|
+
<h2 class="hero-text" data-font-size="subheading" data-axis="horizontal">
|
|
102
|
+
<!--v-if--><span class="hero-text-segment normal">Ready to book your appointment?</span>
|
|
103
103
|
</h2>
|
|
104
104
|
<p class="page-body-normal">Get in touch to book your appointment.</p>
|
|
105
105
|
</div>
|
|
@@ -5,9 +5,9 @@ exports[`ServiceSummary > renders correct HTML structure 1`] = `
|
|
|
5
5
|
<div class="service-summary__grid">
|
|
6
6
|
<div class="service-summary__image-wrapper"><img width="1080" height="1080" data-nuxt-img="" srcset="/_ipx/s_1080x1080/images/test.jpg 1x, /_ipx/s_2160x2160/images/test.jpg 2x" alt="Test Service" loading="eager" fetchpriority="high" class="service-summary__image" src="/_ipx/s_1080x1080/images/test.jpg"></div>
|
|
7
7
|
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
8
|
-
<div class="eyebrow-text large">A subtitle</div>
|
|
9
|
-
<h2 id="v-0-0" class="hero-text mb-20 title axis
|
|
10
|
-
<!--v-if--><span class="text-
|
|
8
|
+
<div class="eyebrow-text" data-font-size="large">A subtitle</div>
|
|
9
|
+
<h2 id="v-0-0" class="hero-text mb-20" data-font-size="title" data-axis="horizontal">
|
|
10
|
+
<!--v-if--><span class="hero-text-segment normal">Test Service</span>
|
|
11
11
|
</h2>
|
|
12
12
|
<div class="service-summary__pills"><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">60 mins</span></span><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">From £50</span></span></div>
|
|
13
13
|
<p class="page-body-normal">What this service is</p>
|
|
@@ -217,14 +217,14 @@ describe("ServicesCard", () => {
|
|
|
217
217
|
const wrapper = await mountSuspended(ServicesCard, {
|
|
218
218
|
props: { serviceData: mockService },
|
|
219
219
|
});
|
|
220
|
-
expect(wrapper.find(".eyebrow-text").
|
|
220
|
+
expect(wrapper.find(".eyebrow-text").attributes("data-font-size")).toBe("large");
|
|
221
221
|
});
|
|
222
222
|
|
|
223
223
|
it("applies eyebrowConfig.fontSize override", async () => {
|
|
224
224
|
const wrapper = await mountSuspended(ServicesCard, {
|
|
225
225
|
props: { serviceData: mockService, eyebrowConfig: { fontSize: "small" } },
|
|
226
226
|
});
|
|
227
|
-
expect(wrapper.find(".eyebrow-text").
|
|
227
|
+
expect(wrapper.find(".eyebrow-text").attributes("data-font-size")).toBe("small");
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
it("applies eyebrowConfig.tag override", async () => {
|
|
@@ -254,7 +254,7 @@ describe("ServicesCard", () => {
|
|
|
254
254
|
const wrapper = await mountSuspended(ServicesCard, {
|
|
255
255
|
props: { serviceData: mockService, heroConfig: { fontSize: "title" } },
|
|
256
256
|
});
|
|
257
|
-
expect(wrapper.find(".hero-text").
|
|
257
|
+
expect(wrapper.find(".hero-text").attributes("data-font-size")).toBe("title");
|
|
258
258
|
});
|
|
259
259
|
|
|
260
260
|
// ─── styleClassPassthrough ──────────────────────────────────────────────
|
|
@@ -7,9 +7,9 @@ exports[`ServicesCard > renders correct HTML structure 1`] = `
|
|
|
7
7
|
<div class="grid-stack__layer"></div>
|
|
8
8
|
</div>
|
|
9
9
|
<div class="services-card-details">
|
|
10
|
-
<div class="eyebrow-text large">A subtitle</div>
|
|
11
|
-
<h2 class="hero-text heading axis
|
|
12
|
-
<!--v-if--><span class="text-
|
|
10
|
+
<div class="eyebrow-text" data-font-size="large">A subtitle</div>
|
|
11
|
+
<h2 class="hero-text" data-font-size="heading" data-axis="horizontal">
|
|
12
|
+
<!--v-if--><span class="hero-text-segment normal">Test Service</span>
|
|
13
13
|
</h2>
|
|
14
14
|
<div class="services-card-description">A short description of the service.</div>
|
|
15
15
|
<div class="services-card-footer">
|
|
@@ -139,7 +139,7 @@ describe("ServicesCardGrid", () => {
|
|
|
139
139
|
props: { servicesData: mockServices, eyebrowConfig: { fontSize: "small" } },
|
|
140
140
|
});
|
|
141
141
|
wrapper.findAll(".eyebrow-text").forEach((el) => {
|
|
142
|
-
expect(el.
|
|
142
|
+
expect(el.attributes("data-font-size")).toBe("small");
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
|
|
@@ -6,9 +6,9 @@ exports[`ServiceSummaryGrid > renders correct HTML structure 1`] = `
|
|
|
6
6
|
<div class="service-summary__grid">
|
|
7
7
|
<div class="service-summary__image-wrapper"><img width="1080" height="1080" data-nuxt-img="" srcset="/_ipx/s_1080x1080/images/locs-installation.jpg 1x, /_ipx/s_2160x2160/images/locs-installation.jpg 2x" alt="Locs Installation" loading="eager" fetchpriority="high" class="service-summary__image" src="/_ipx/s_1080x1080/images/locs-installation.jpg"></div>
|
|
8
8
|
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
9
|
-
<div class="eyebrow-text large">Subtitle for Locs Installation</div>
|
|
10
|
-
<h2 id="v-0-0-0" class="hero-text mb-20 title axis
|
|
11
|
-
<!--v-if--><span class="text-
|
|
9
|
+
<div class="eyebrow-text" data-font-size="large">Subtitle for Locs Installation</div>
|
|
10
|
+
<h2 id="v-0-0-0" class="hero-text mb-20" data-font-size="title" data-axis="horizontal">
|
|
11
|
+
<!--v-if--><span class="hero-text-segment normal">Locs Installation</span>
|
|
12
12
|
</h2>
|
|
13
13
|
<div class="service-summary__pills"><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">60 mins</span></span><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">From £50</span></span></div>
|
|
14
14
|
<p class="page-body-normal">What this service is.</p>
|
|
@@ -19,9 +19,9 @@ exports[`ServiceSummaryGrid > renders correct HTML structure 1`] = `
|
|
|
19
19
|
<div class="service-summary__grid">
|
|
20
20
|
<div class="service-summary__image-wrapper"><img width="1080" height="1080" data-nuxt-img="" srcset="/_ipx/s_1080x1080/images/locs-retwist.jpg 1x, /_ipx/s_2160x2160/images/locs-retwist.jpg 2x" alt="Locs Retwist" loading="eager" fetchpriority="auto" class="service-summary__image" src="/_ipx/s_1080x1080/images/locs-retwist.jpg"></div>
|
|
21
21
|
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
22
|
-
<div class="eyebrow-text large">Subtitle for Locs Retwist</div>
|
|
23
|
-
<h2 id="v-0-0-1" class="hero-text mb-20 title axis
|
|
24
|
-
<!--v-if--><span class="text-
|
|
22
|
+
<div class="eyebrow-text" data-font-size="large">Subtitle for Locs Retwist</div>
|
|
23
|
+
<h2 id="v-0-0-1" class="hero-text mb-20" data-font-size="title" data-axis="horizontal">
|
|
24
|
+
<!--v-if--><span class="hero-text-segment normal">Locs Retwist</span>
|
|
25
25
|
</h2>
|
|
26
26
|
<div class="service-summary__pills"><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">60 mins</span></span><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">From £50</span></span></div>
|
|
27
27
|
<p class="page-body-normal">What this service is.</p>
|
|
@@ -32,9 +32,9 @@ exports[`ServiceSummaryGrid > renders correct HTML structure 1`] = `
|
|
|
32
32
|
<div class="service-summary__grid">
|
|
33
33
|
<div class="service-summary__image-wrapper"><img width="1080" height="1080" data-nuxt-img="" srcset="/_ipx/s_1080x1080/images/colour-treatment.jpg 1x, /_ipx/s_2160x2160/images/colour-treatment.jpg 2x" alt="Colour Treatment" loading="lazy" fetchpriority="auto" class="service-summary__image" src="/_ipx/s_1080x1080/images/colour-treatment.jpg"></div>
|
|
34
34
|
<div class="service-summary__info-wrapper service-summary__info-wrapper--align-center">
|
|
35
|
-
<div class="eyebrow-text large">Subtitle for Colour Treatment</div>
|
|
36
|
-
<h2 id="v-0-0-2" class="hero-text mb-20 title axis
|
|
37
|
-
<!--v-if--><span class="text-
|
|
35
|
+
<div class="eyebrow-text" data-font-size="large">Subtitle for Colour Treatment</div>
|
|
36
|
+
<h2 id="v-0-0-2" class="hero-text mb-20" data-font-size="title" data-axis="horizontal">
|
|
37
|
+
<!--v-if--><span class="hero-text-segment normal">Colour Treatment</span>
|
|
38
38
|
</h2>
|
|
39
39
|
<div class="service-summary__pills"><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">60 mins</span></span><span class="display-pill" data-size="md" data-variant="neutral"><!--v-if--><span class="display-pill-label">From £50</span></span></div>
|
|
40
40
|
<p class="page-body-normal">What this service is.</p>
|
|
@@ -8,9 +8,9 @@ exports[`ServicesCardGrid > renders correct HTML structure 1`] = `
|
|
|
8
8
|
<div class="grid-stack__layer"></div>
|
|
9
9
|
</div>
|
|
10
10
|
<div class="services-card-details">
|
|
11
|
-
<div class="eyebrow-text large">Subtitle for Locs Installation</div>
|
|
12
|
-
<h2 class="hero-text heading axis
|
|
13
|
-
<!--v-if--><span class="text-
|
|
11
|
+
<div class="eyebrow-text" data-font-size="large">Subtitle for Locs Installation</div>
|
|
12
|
+
<h2 class="hero-text" data-font-size="heading" data-axis="horizontal">
|
|
13
|
+
<!--v-if--><span class="hero-text-segment normal">Locs Installation</span>
|
|
14
14
|
</h2>
|
|
15
15
|
<div class="services-card-description">Short description for Locs Installation.</div>
|
|
16
16
|
<div class="services-card-footer">
|
|
@@ -31,9 +31,9 @@ exports[`ServicesCardGrid > renders correct HTML structure 1`] = `
|
|
|
31
31
|
<div class="grid-stack__layer"></div>
|
|
32
32
|
</div>
|
|
33
33
|
<div class="services-card-details">
|
|
34
|
-
<div class="eyebrow-text large">Subtitle for Locs Retwist</div>
|
|
35
|
-
<h2 class="hero-text heading axis
|
|
36
|
-
<!--v-if--><span class="text-
|
|
34
|
+
<div class="eyebrow-text" data-font-size="large">Subtitle for Locs Retwist</div>
|
|
35
|
+
<h2 class="hero-text" data-font-size="heading" data-axis="horizontal">
|
|
36
|
+
<!--v-if--><span class="hero-text-segment normal">Locs Retwist</span>
|
|
37
37
|
</h2>
|
|
38
38
|
<div class="services-card-description">Short description for Locs Retwist.</div>
|
|
39
39
|
<div class="services-card-footer">
|
|
@@ -54,9 +54,9 @@ exports[`ServicesCardGrid > renders correct HTML structure 1`] = `
|
|
|
54
54
|
<div class="grid-stack__layer"></div>
|
|
55
55
|
</div>
|
|
56
56
|
<div class="services-card-details">
|
|
57
|
-
<div class="eyebrow-text large">Subtitle for Colour Treatment</div>
|
|
58
|
-
<h2 class="hero-text heading axis
|
|
59
|
-
<!--v-if--><span class="text-
|
|
57
|
+
<div class="eyebrow-text" data-font-size="large">Subtitle for Colour Treatment</div>
|
|
58
|
+
<h2 class="hero-text" data-font-size="heading" data-axis="horizontal">
|
|
59
|
+
<!--v-if--><span class="hero-text-segment normal">Colour Treatment</span>
|
|
60
60
|
</h2>
|
|
61
61
|
<div class="services-card-description">Short description for Colour Treatment.</div>
|
|
62
62
|
<div class="services-card-footer">
|