srcdev-nuxt-components 9.1.21 → 9.1.23
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/settings.json +6 -1
- package/.claude/skills/components/services-section-grid.md +130 -0
- package/.claude/skills/components/services-section.md +16 -0
- package/.claude/skills/index.md +3 -2
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +2 -0
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +193 -0
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +202 -0
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +12 -0
- package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -17
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +181 -0
- package/app/components/03.organisms/services/services-grids/stories/ServicesSectionGrid.stories.ts +196 -0
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +180 -0
- package/app/components/03.organisms/services/services-grids/tests/ServicesSectionGrid.spec.ts +167 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +42 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesSectionGrid.spec.ts.snap +87 -0
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +6 -2
- package/app/components/03.organisms/services/services-section/stories/ServicesSection.stories.ts +214 -0
- package/app/components/03.organisms/services/services-section/tests/ServicesSection.spec.ts +213 -0
- package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +84 -0
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import ServicesSection from "../ServicesSection.vue";
|
|
4
|
+
import type { Service } from "~/types/types.services";
|
|
5
|
+
|
|
6
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const mockService: Service = {
|
|
9
|
+
slug: "test-service",
|
|
10
|
+
title: "Test Service",
|
|
11
|
+
subtitle: "A subtitle",
|
|
12
|
+
price: "£50",
|
|
13
|
+
duration: "60 mins",
|
|
14
|
+
image: "/images/test.jpg",
|
|
15
|
+
shortDescription: "Short description",
|
|
16
|
+
longDescription: "Long description text",
|
|
17
|
+
heroHeading: [{ text: "A great heading", styleClass: "normal" }],
|
|
18
|
+
whatIsIt: "What this service is",
|
|
19
|
+
process: ["Step one", "Step two"],
|
|
20
|
+
idealFor: ["Person A", "Person B"],
|
|
21
|
+
maintenance: "Maintenance advice",
|
|
22
|
+
faqs: [{ question: "FAQ question?", answer: "FAQ answer." }],
|
|
23
|
+
seoTitle: "SEO Title",
|
|
24
|
+
seoDescription: "SEO description",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("ServicesSection", () => {
|
|
30
|
+
// ─── Mount ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
it("mounts without error", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
34
|
+
props: { serviceData: mockService },
|
|
35
|
+
});
|
|
36
|
+
expect(wrapper.vm).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders correct HTML structure", async () => {
|
|
40
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
41
|
+
props: { serviceData: mockService },
|
|
42
|
+
});
|
|
43
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ─── Root element ───────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
it("has services-section class on root", async () => {
|
|
49
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
50
|
+
props: { serviceData: mockService },
|
|
51
|
+
});
|
|
52
|
+
expect(wrapper.classes()).toContain("services-section");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders as a div by default", async () => {
|
|
56
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
57
|
+
props: { serviceData: mockService },
|
|
58
|
+
});
|
|
59
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders the tag prop as the root element", async () => {
|
|
63
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
64
|
+
props: { serviceData: mockService, tag: "section" },
|
|
65
|
+
});
|
|
66
|
+
expect(wrapper.element.tagName).toBe("SECTION");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ─── Icons ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
it("renders the default duration icon name in the template", async () => {
|
|
72
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
73
|
+
props: { serviceData: mockService },
|
|
74
|
+
});
|
|
75
|
+
expect(wrapper.html()).toContain("mdi:clock-time-four-outline");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders the default price icon name in the template", async () => {
|
|
79
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
80
|
+
props: { serviceData: mockService },
|
|
81
|
+
});
|
|
82
|
+
expect(wrapper.html()).toContain("mdi:currency-gbp");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("overrides the duration icon via durationIcon prop", async () => {
|
|
86
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
87
|
+
props: { serviceData: mockService, durationIcon: "mdi:timer-outline" },
|
|
88
|
+
});
|
|
89
|
+
expect(wrapper.html()).toContain("mdi:timer-outline");
|
|
90
|
+
expect(wrapper.html()).not.toContain("mdi:clock-time-four-outline");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("overrides the price icon via priceIcon prop", async () => {
|
|
94
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
95
|
+
props: { serviceData: mockService, priceIcon: "mdi:currency-usd" },
|
|
96
|
+
});
|
|
97
|
+
expect(wrapper.html()).toContain("mdi:currency-usd");
|
|
98
|
+
expect(wrapper.html()).not.toContain("mdi:currency-gbp");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("both icons can be overridden together", async () => {
|
|
102
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
103
|
+
props: {
|
|
104
|
+
serviceData: mockService,
|
|
105
|
+
durationIcon: "mdi:timer-outline",
|
|
106
|
+
priceIcon: "mdi:currency-eur",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(wrapper.html()).toContain("mdi:timer-outline");
|
|
110
|
+
expect(wrapper.html()).toContain("mdi:currency-eur");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── Service data ───────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
it("renders service duration text", async () => {
|
|
116
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
117
|
+
props: { serviceData: mockService },
|
|
118
|
+
});
|
|
119
|
+
expect(wrapper.text()).toContain(mockService.duration);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("renders service price text", async () => {
|
|
123
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
124
|
+
props: { serviceData: mockService },
|
|
125
|
+
});
|
|
126
|
+
expect(wrapper.text()).toContain(mockService.price);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ─── Summary mode ───────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
it("renders summary-link slot in summary mode", async () => {
|
|
132
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
133
|
+
props: { serviceData: mockService, isSummary: true },
|
|
134
|
+
slots: {
|
|
135
|
+
"summary-link": '<a class="test-link" href="/services/test">View service</a>',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
expect(wrapper.find(".test-link").exists()).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("does not render summary-link slot in full mode", async () => {
|
|
142
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
143
|
+
props: { serviceData: mockService, isSummary: false },
|
|
144
|
+
slots: {
|
|
145
|
+
"summary-link": '<a class="test-link" href="/services/test">View service</a>',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
expect(wrapper.find(".test-link").exists()).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders cta slot in full mode", async () => {
|
|
152
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
153
|
+
props: { serviceData: mockService, isSummary: false },
|
|
154
|
+
slots: {
|
|
155
|
+
cta: '<button class="test-cta">Book now</button>',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
expect(wrapper.find(".test-cta").exists()).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("does not render cta slot in summary mode", async () => {
|
|
162
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
163
|
+
props: { serviceData: mockService, isSummary: true },
|
|
164
|
+
slots: {
|
|
165
|
+
cta: '<button class="test-cta">Book now</button>',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
expect(wrapper.find(".test-cta").exists()).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── Reverse ────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
it("applies reverse class when reverse prop is true", async () => {
|
|
174
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
175
|
+
props: { serviceData: mockService, reverse: true },
|
|
176
|
+
});
|
|
177
|
+
expect(wrapper.find(".services-section__grid").classes()).toContain("services-section__grid--reverse");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not apply reverse class by default", async () => {
|
|
181
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
182
|
+
props: { serviceData: mockService },
|
|
183
|
+
});
|
|
184
|
+
expect(wrapper.find(".services-section__grid").classes()).not.toContain("services-section__grid--reverse");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─── styleClassPassthrough ──────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
190
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
191
|
+
props: { serviceData: mockService, styleClassPassthrough: "custom-class" },
|
|
192
|
+
});
|
|
193
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
197
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
198
|
+
props: { serviceData: mockService, styleClassPassthrough: ["class-a", "class-b"] },
|
|
199
|
+
});
|
|
200
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
201
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
205
|
+
const wrapper = await mountSuspended(ServicesSection, {
|
|
206
|
+
props: { serviceData: mockService, styleClassPassthrough: ["original"] },
|
|
207
|
+
});
|
|
208
|
+
expect(wrapper.classes()).toContain("original");
|
|
209
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
210
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
211
|
+
expect(wrapper.classes()).toContain("updated");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ServicesSection > renders correct HTML structure 1`] = `
|
|
4
|
+
"<div class="services-section">
|
|
5
|
+
<div class="services-section__grid">
|
|
6
|
+
<div class="image-wrapper"><img data-nuxt-img="" srcset="/_ipx/_/images/test.jpg 1x, /_ipx/_/images/test.jpg 2x" alt="Test Service" loading="eager" class="image" src="/_ipx/_/images/test.jpg"></div>
|
|
7
|
+
<div class="info-wrapper">
|
|
8
|
+
<div class="eyebrow-text large">A subtitle</div>
|
|
9
|
+
<h2 class="hero-text mb-20 title axis-horizontal">
|
|
10
|
+
<!--v-if--><span class="text-block-0 normal">Test Service</span>
|
|
11
|
+
</h2>
|
|
12
|
+
<div class="price-duration">
|
|
13
|
+
<div class="flex-row"><span class="iconify i-mdi:clock-time-four-outline decorator" aria-hidden="true"></span><span>60 mins</span></div>
|
|
14
|
+
<div class="flex-row"><span class="iconify i-mdi:currency-gbp decorator" aria-hidden="true"></span><span>£50</span></div>
|
|
15
|
+
</div>
|
|
16
|
+
<p class="page-body-normal">Long description text</p>
|
|
17
|
+
<h2 class="hero-text mb-20 subheading axis-horizontal">
|
|
18
|
+
<!--v-if--><span class="text-block-0 normal">A great heading</span>
|
|
19
|
+
</h2>
|
|
20
|
+
<p class="page-body-normal">What this service is</p>
|
|
21
|
+
<!--v-if-->
|
|
22
|
+
<h2 class="hero-text mb-20 subheading axis-horizontal">
|
|
23
|
+
<!--v-if--><span class="text-block-0 normal">The Process</span>
|
|
24
|
+
</h2>
|
|
25
|
+
<ol class="stepper-list has-connectors" show-connectors="true">
|
|
26
|
+
<li class="indicator-top indicator-circle" style="--_connector-top: 0px; --_connector-height: 0px;">
|
|
27
|
+
<div class="stepper-list__indicator-counter"></div>
|
|
28
|
+
<div>
|
|
29
|
+
<p class="page-body-normal">Step one</p>
|
|
30
|
+
</div>
|
|
31
|
+
</li>
|
|
32
|
+
<li class="indicator-top indicator-circle">
|
|
33
|
+
<div class="stepper-list__indicator-counter"></div>
|
|
34
|
+
<div>
|
|
35
|
+
<p class="page-body-normal">Step two</p>
|
|
36
|
+
</div>
|
|
37
|
+
</li>
|
|
38
|
+
</ol>
|
|
39
|
+
<h2 class="hero-text mb-20 subheading axis-horizontal">
|
|
40
|
+
<!--v-if--><span class="text-block-0 normal">Ideal For</span>
|
|
41
|
+
</h2>
|
|
42
|
+
<ul class="stepper-list">
|
|
43
|
+
<li class="has-indicator indicator-top indicator-disc">
|
|
44
|
+
<div class="stepper-list__indicator-custom"><span class="iconify i-mdi:checkbox-marked-circle-outline indicator-icon" aria-hidden="true"></span></div>
|
|
45
|
+
<div>
|
|
46
|
+
<p class="page-body-normal">Person A</p>
|
|
47
|
+
</div>
|
|
48
|
+
</li>
|
|
49
|
+
<li class="has-indicator indicator-top indicator-disc">
|
|
50
|
+
<div class="stepper-list__indicator-custom"><span class="iconify i-mdi:checkbox-marked-circle-outline indicator-icon" aria-hidden="true"></span></div>
|
|
51
|
+
<div>
|
|
52
|
+
<p class="page-body-normal">Person B</p>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
</ul>
|
|
56
|
+
<h2 class="hero-text mb-20 subheading axis-horizontal">
|
|
57
|
+
<!--v-if--><span class="text-block-0 normal">Aftercare & Maintenance</span>
|
|
58
|
+
</h2>
|
|
59
|
+
<p class="page-body-normal">Maintenance advice</p>
|
|
60
|
+
<h2 class="hero-text mb-20 subheading axis-horizontal">
|
|
61
|
+
<!--v-if--><span class="text-block-0 normal">Frequently Asked Questions</span>
|
|
62
|
+
</h2>
|
|
63
|
+
<div class="display-accordian services-faq" id="faq">
|
|
64
|
+
<div class="expanding-panel accordian-item" icon-size="medium">
|
|
65
|
+
<details class="expanding-panel-details" name="faq-v-0-0-0">
|
|
66
|
+
<summary id="id-faq-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-faq-v-0-0-0-content" aria-expanded="false"><span class="label-wrapper">FAQ question?</span><span class="icon-wrapper"><span class="iconify i-mdi:chevron-down" aria-hidden="true"></span></span></summary>
|
|
67
|
+
</details>
|
|
68
|
+
<div id="id-faq-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-faq-v-0-0-0-trigger" role="region">
|
|
69
|
+
<div class="inner">
|
|
70
|
+
<p>FAQ answer.</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="glass-panel p-24">
|
|
76
|
+
<h2 class="hero-text mbs-0 mbe-20 subheading axis-horizontal">
|
|
77
|
+
<!--v-if--><span class="text-block-0 normal">Ready to book your highlights appointment?</span>
|
|
78
|
+
</h2>
|
|
79
|
+
<p class="page-body-normal">Mobile service across Bath — I come to you.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>"
|
|
84
|
+
`;
|