srcdev-nuxt-components 9.6.13 → 9.6.14
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/storybook-add-story.md +5 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- 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/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/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/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/package.json +1 -1
|
@@ -7,134 +7,131 @@ const meta: Meta<typeof LinkText> = {
|
|
|
7
7
|
argTypes: {
|
|
8
8
|
to: {
|
|
9
9
|
control: "text",
|
|
10
|
-
description: "Link destination (internal path or external URL)",
|
|
10
|
+
description: "Link destination (internal path or external URL).",
|
|
11
|
+
table: { category: "Basic" },
|
|
11
12
|
},
|
|
12
13
|
linkText: {
|
|
13
14
|
control: "text",
|
|
14
|
-
description: "Visible link label",
|
|
15
|
+
description: "Visible link label. It should name the destination, so it's never clamped.",
|
|
16
|
+
table: { category: "Content" },
|
|
15
17
|
},
|
|
16
18
|
external: {
|
|
17
19
|
control: "boolean",
|
|
18
|
-
description: "Force external link handling",
|
|
19
|
-
|
|
20
|
+
description: "Force external link handling on an internal-looking path.",
|
|
21
|
+
table: { category: "Basic" },
|
|
20
22
|
},
|
|
21
23
|
target: {
|
|
22
24
|
control: { type: "select" },
|
|
23
25
|
options: [undefined, "_blank", "_self"],
|
|
24
|
-
description: "Link target
|
|
25
|
-
|
|
26
|
-
styleClassPassthrough: {
|
|
27
|
-
control: "object",
|
|
28
|
-
description: "Additional classes",
|
|
29
|
-
defaultValue: [],
|
|
26
|
+
description: "Link `target`. NuxtLink adds `rel=\"noopener noreferrer\"` for an external `_blank`.",
|
|
27
|
+
table: { category: "Basic" },
|
|
30
28
|
},
|
|
29
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
31
30
|
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export default meta;
|
|
35
|
-
type Story = StoryObj<typeof LinkText>;
|
|
36
|
-
|
|
37
|
-
export const Default: Story = {
|
|
38
31
|
args: {
|
|
39
32
|
to: "/",
|
|
40
33
|
linkText: "Read More",
|
|
41
34
|
external: false,
|
|
42
|
-
styleClassPassthrough: [],
|
|
43
35
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
parameters: {
|
|
37
|
+
docs: {
|
|
38
|
+
description: {
|
|
39
|
+
component:
|
|
40
|
+
"A `NuxtLink` styled as a standalone text link. `to` and `linkText` are required; `left` and `right` slots take icons, which stay centred on the label. Colours, underline and gap come from `--link-text-*` tokens.",
|
|
41
|
+
},
|
|
48
42
|
},
|
|
49
|
-
template: `<LinkText v-bind="args" />`,
|
|
50
|
-
}),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const WithLeftIcon: Story = {
|
|
54
|
-
args: {
|
|
55
|
-
to: "/",
|
|
56
|
-
linkText: "Go Back",
|
|
57
|
-
styleClassPassthrough: [],
|
|
58
43
|
},
|
|
59
|
-
render: (args) => ({
|
|
60
|
-
components: { LinkText },
|
|
61
|
-
setup() {
|
|
62
|
-
return { args };
|
|
63
|
-
},
|
|
64
|
-
template: `
|
|
65
|
-
<LinkText v-bind="args">
|
|
66
|
-
<template #left>
|
|
67
|
-
<Icon name="lucide:arrow-left" />
|
|
68
|
-
</template>
|
|
69
|
-
</LinkText>
|
|
70
|
-
`,
|
|
71
|
-
}),
|
|
72
44
|
};
|
|
73
45
|
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
to: "/",
|
|
77
|
-
linkText: "Learn More",
|
|
78
|
-
styleClassPassthrough: [],
|
|
79
|
-
},
|
|
80
|
-
render: (args) => ({
|
|
81
|
-
components: { LinkText },
|
|
82
|
-
setup() {
|
|
83
|
-
return { args };
|
|
84
|
-
},
|
|
85
|
-
template: `
|
|
86
|
-
<LinkText v-bind="args">
|
|
87
|
-
<template #right>
|
|
88
|
-
<Icon name="lucide:arrow-right" />
|
|
89
|
-
</template>
|
|
90
|
-
</LinkText>
|
|
91
|
-
`,
|
|
92
|
-
}),
|
|
93
|
-
};
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof LinkText>;
|
|
94
48
|
|
|
95
|
-
|
|
96
|
-
args
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
49
|
+
const withIcons = (left: string | null, right: string | null) => `
|
|
50
|
+
<LinkText v-bind="args">
|
|
51
|
+
${left ? `<template #left><Icon name="${left}" /></template>` : ""}
|
|
52
|
+
${right ? `<template #right><Icon name="${right}" /></template>` : ""}
|
|
53
|
+
</LinkText>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const story = (template: string, description: string, args: Story["args"] = {}): Story => ({
|
|
57
|
+
args,
|
|
58
|
+
render: (storyArgs) => ({
|
|
102
59
|
components: { LinkText },
|
|
103
60
|
setup() {
|
|
104
|
-
return { args };
|
|
61
|
+
return { args: storyArgs };
|
|
105
62
|
},
|
|
106
|
-
template
|
|
107
|
-
<LinkText v-bind="args">
|
|
108
|
-
<template #left>
|
|
109
|
-
<Icon name="lucide:sparkles" />
|
|
110
|
-
</template>
|
|
111
|
-
<template #right>
|
|
112
|
-
<Icon name="lucide:arrow-right" />
|
|
113
|
-
</template>
|
|
114
|
-
</LinkText>
|
|
115
|
-
`,
|
|
63
|
+
template,
|
|
116
64
|
}),
|
|
117
|
-
}
|
|
65
|
+
parameters: { docs: { description: { story: description } } },
|
|
66
|
+
});
|
|
118
67
|
|
|
119
|
-
export const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
68
|
+
export const Default = story(withIcons(null, null), "A plain link. Use the Controls panel to change the destination, label and target.");
|
|
69
|
+
|
|
70
|
+
export const WithLeftIcon = story(withIcons("lucide:arrow-left", null), "A `left` slot icon for a back or previous link.", {
|
|
71
|
+
linkText: "Go Back",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const WithRightIcon = story(withIcons(null, "lucide:arrow-right"), "A `right` slot icon for a forward or more link.", {
|
|
75
|
+
linkText: "Learn More",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const WithBothIcons = story(withIcons("lucide:sparkles", "lucide:arrow-right"), "Both slots. Left icon, label, then right icon.", {
|
|
79
|
+
linkText: "Explore",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const ExternalLink = story(
|
|
83
|
+
withIcons(null, "lucide:external-link"),
|
|
84
|
+
"An external link opening in a new tab, with an icon that says so.",
|
|
85
|
+
{ to: "https://nuxt.com", linkText: "Nuxt Docs", external: true, target: "_blank" }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const StressTest: Story = {
|
|
89
|
+
name: "Stress Test (Worst-Case Data)",
|
|
90
|
+
render: () => ({
|
|
128
91
|
components: { LinkText },
|
|
129
|
-
setup() {
|
|
130
|
-
return { args };
|
|
131
|
-
},
|
|
132
92
|
template: `
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
93
|
+
<div style="display: grid; gap: 2.4rem; padding: 2.4rem; font-size: 1.6rem;">
|
|
94
|
+
<p style="margin: 0; font-size: 1.4rem;">Long German label, an unbroken label and a long URL as the label, each with icons, in an 18rem column.</p>
|
|
95
|
+
<div style="display: grid; gap: 1.6rem; justify-items: start; inline-size: 18rem; outline: 0.1rem dashed currentColor;">
|
|
96
|
+
<LinkText to="/" link-text="Alle Lieferbedingungen und Rückgaberichtlinien ansehen">
|
|
97
|
+
<template #right><Icon name="lucide:arrow-right" /></template>
|
|
98
|
+
</LinkText>
|
|
99
|
+
<LinkText to="/" link-text="Lieferbedingungenundrückgaberichtlinienansehen">
|
|
100
|
+
<template #left><Icon name="lucide:arrow-left" /></template>
|
|
101
|
+
<template #right><Icon name="lucide:arrow-right" /></template>
|
|
102
|
+
</LinkText>
|
|
103
|
+
<LinkText to="https://example.invalid/a/very/long/unbroken/url" link-text="https://example.invalid/a/very/long/unbroken/url" external target="_blank">
|
|
104
|
+
<template #right><Icon name="lucide:external-link" /></template>
|
|
105
|
+
</LinkText>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<p style="margin: 0; font-size: 1.4rem;">Emoji first, HTML-like text (shown as text), right-to-left with a right icon, and a single character.</p>
|
|
109
|
+
<div style="display: flex; flex-wrap: wrap; gap: 2.4rem; align-items: center;">
|
|
110
|
+
<LinkText to="/" link-text="😀 Emoji first" />
|
|
111
|
+
<LinkText to="/" link-text="<b>not bold</b>" />
|
|
112
|
+
<LinkText to="/" link-text="اقرأ المزيد" dir="rtl">
|
|
113
|
+
<template #right><Icon name="lucide:arrow-left" /></template>
|
|
114
|
+
</LinkText>
|
|
115
|
+
<LinkText to="/" link-text="A" />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<p style="margin: 0; font-size: 1.4rem;">Icon-only link: empty <code>link-text</code>, named with <code>aria-label</code>. Tab to it to check the focus ring.</p>
|
|
119
|
+
<LinkText to="/" link-text="" aria-label="Back to home">
|
|
120
|
+
<template #left><Icon name="lucide:house" /></template>
|
|
121
|
+
</LinkText>
|
|
122
|
+
|
|
123
|
+
<p style="margin: 0; font-size: 1.4rem;">Inside a sentence: the link sits on the text line.</p>
|
|
124
|
+
<p style="margin: 0;">Our full price list is on the <LinkText to="/" link-text="services page" /> and updated monthly.</p>
|
|
125
|
+
</div>
|
|
138
126
|
`,
|
|
139
127
|
}),
|
|
128
|
+
parameters: {
|
|
129
|
+
controls: { disable: true },
|
|
130
|
+
docs: {
|
|
131
|
+
description: {
|
|
132
|
+
story:
|
|
133
|
+
"Worst-case labels. Check: long and unbroken labels and URLs wrap inside the dashed 18rem column with their icons centred on the wrapped text; emoji and HTML-like text render as text; right-to-left puts the right slot on the inline end; an icon-only link keeps a visible focus ring; an inline link sits on the line of text.",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
140
137
|
};
|
|
@@ -1,177 +1,168 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import LinkText from "../LinkText.vue";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type LinkTextProps = InstanceType<typeof LinkText>["$props"];
|
|
6
|
+
|
|
7
|
+
const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
|
|
8
|
+
|
|
9
|
+
const mountLink = async (props: Partial<LinkTextProps> = {}, slots: Record<string, string> = {}) => {
|
|
10
|
+
const wrapper = await mountSuspended(LinkText, {
|
|
11
|
+
props: { to: "/about", linkText: "About Us", ...props } as LinkTextProps,
|
|
12
|
+
slots,
|
|
13
|
+
});
|
|
14
|
+
mounted.push(wrapper);
|
|
15
|
+
return wrapper;
|
|
16
|
+
};
|
|
7
17
|
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("LinkText", () => {
|
|
8
24
|
it("mounts without error", async () => {
|
|
9
|
-
const wrapper = await
|
|
10
|
-
props: { to: "/about", linkText: "About Us" },
|
|
11
|
-
});
|
|
25
|
+
const wrapper = await mountLink();
|
|
12
26
|
expect(wrapper.vm).toBeTruthy();
|
|
13
27
|
});
|
|
14
28
|
|
|
15
29
|
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
16
30
|
|
|
17
31
|
it("renders correct HTML structure with default props", async () => {
|
|
18
|
-
const wrapper = await
|
|
19
|
-
props: { to: "/about", linkText: "About Us" },
|
|
20
|
-
});
|
|
32
|
+
const wrapper = await mountLink();
|
|
21
33
|
expect(wrapper.html()).toMatchSnapshot();
|
|
22
34
|
});
|
|
23
35
|
|
|
24
36
|
it("renders correct HTML structure with left slot", async () => {
|
|
25
|
-
const wrapper = await
|
|
26
|
-
props: { to: "/about", linkText: "About Us" },
|
|
27
|
-
slots: { left: "<svg data-testid='icon-left' />" },
|
|
28
|
-
});
|
|
37
|
+
const wrapper = await mountLink({}, { left: "<svg data-testid='icon-left' />" });
|
|
29
38
|
expect(wrapper.html()).toMatchSnapshot();
|
|
30
39
|
});
|
|
31
40
|
|
|
32
41
|
it("renders correct HTML structure with right slot", async () => {
|
|
33
|
-
const wrapper = await
|
|
34
|
-
props: { to: "/about", linkText: "About Us" },
|
|
35
|
-
slots: { right: "<svg data-testid='icon-right' />" },
|
|
36
|
-
});
|
|
42
|
+
const wrapper = await mountLink({}, { right: "<svg data-testid='icon-right' />" });
|
|
37
43
|
expect(wrapper.html()).toMatchSnapshot();
|
|
38
44
|
});
|
|
39
45
|
|
|
40
46
|
it("renders correct HTML structure with both slots", async () => {
|
|
41
|
-
const wrapper = await
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
right: "<svg data-testid='icon-right' />",
|
|
46
|
-
},
|
|
47
|
-
});
|
|
47
|
+
const wrapper = await mountLink(
|
|
48
|
+
{},
|
|
49
|
+
{ left: "<svg data-testid='icon-left' />", right: "<svg data-testid='icon-right' />" }
|
|
50
|
+
);
|
|
48
51
|
expect(wrapper.html()).toMatchSnapshot();
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
it("renders correct HTML structure with all props set", async () => {
|
|
52
|
-
const wrapper = await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
styleClassPassthrough: ["custom-class"],
|
|
59
|
-
},
|
|
55
|
+
const wrapper = await mountLink({
|
|
56
|
+
to: "https://example.com",
|
|
57
|
+
linkText: "Visit Us",
|
|
58
|
+
external: true,
|
|
59
|
+
target: "_blank",
|
|
60
|
+
styleClassPassthrough: ["custom-class"],
|
|
60
61
|
});
|
|
61
62
|
expect(wrapper.html()).toMatchSnapshot();
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
// ───
|
|
65
|
+
// ─── Root and label ──────────────────────────────────────────────────────
|
|
65
66
|
|
|
66
67
|
it("always has the link-text class", async () => {
|
|
67
|
-
const wrapper = await
|
|
68
|
-
props: { to: "/", linkText: "Home" },
|
|
69
|
-
});
|
|
68
|
+
const wrapper = await mountLink({ to: "/", linkText: "Home" });
|
|
70
69
|
expect(wrapper.classes()).toContain("link-text");
|
|
71
70
|
});
|
|
72
71
|
|
|
73
|
-
// ─── linkText ────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
72
|
it("renders the linkText in a label span", async () => {
|
|
76
|
-
const wrapper = await
|
|
77
|
-
props: { to: "/", linkText: "Read More" },
|
|
78
|
-
});
|
|
73
|
+
const wrapper = await mountLink({ linkText: "Read More" });
|
|
79
74
|
expect(wrapper.find(".link-text__label").text()).toBe("Read More");
|
|
80
75
|
});
|
|
81
76
|
|
|
82
77
|
it("renders linkText with special characters", async () => {
|
|
83
|
-
const wrapper = await
|
|
84
|
-
props: { to: "/", linkText: "Édition & Co." },
|
|
85
|
-
});
|
|
78
|
+
const wrapper = await mountLink({ linkText: "Édition & Co." });
|
|
86
79
|
expect(wrapper.find(".link-text__label").text()).toBe("Édition & Co.");
|
|
87
80
|
});
|
|
88
81
|
|
|
82
|
+
it("renders HTML-like linkText as text", async () => {
|
|
83
|
+
const wrapper = await mountLink({ linkText: "<b>not bold</b>" });
|
|
84
|
+
expect(wrapper.find("b").exists()).toBe(false);
|
|
85
|
+
expect(wrapper.find(".link-text__label").text()).toBe("<b>not bold</b>");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("updates the label when linkText changes after mount", async () => {
|
|
89
|
+
const wrapper = await mountLink({ linkText: "Before" });
|
|
90
|
+
await wrapper.setProps({ linkText: "After" });
|
|
91
|
+
expect(wrapper.find(".link-text__label").text()).toBe("After");
|
|
92
|
+
});
|
|
93
|
+
|
|
89
94
|
// ─── Slots ───────────────────────────────────────────────────────────────
|
|
90
95
|
|
|
91
96
|
it("renders no icon spans when no slots provided", async () => {
|
|
92
|
-
const wrapper = await
|
|
93
|
-
props: { to: "/", linkText: "No Icons" },
|
|
94
|
-
});
|
|
97
|
+
const wrapper = await mountLink();
|
|
95
98
|
expect(wrapper.find(".link-text__icon--left").exists()).toBe(false);
|
|
96
99
|
expect(wrapper.find(".link-text__icon--right").exists()).toBe(false);
|
|
97
100
|
});
|
|
98
101
|
|
|
99
102
|
it("renders left icon span when left slot is provided", async () => {
|
|
100
|
-
const wrapper = await
|
|
101
|
-
props: { to: "/", linkText: "With Left Icon" },
|
|
102
|
-
slots: { left: "<svg />" },
|
|
103
|
-
});
|
|
103
|
+
const wrapper = await mountLink({}, { left: "<svg />" });
|
|
104
104
|
expect(wrapper.find(".link-text__icon--left").exists()).toBe(true);
|
|
105
105
|
expect(wrapper.find(".link-text__icon--right").exists()).toBe(false);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
it("renders right icon span when right slot is provided", async () => {
|
|
109
|
-
const wrapper = await
|
|
110
|
-
props: { to: "/", linkText: "With Right Icon" },
|
|
111
|
-
slots: { right: "<svg />" },
|
|
112
|
-
});
|
|
109
|
+
const wrapper = await mountLink({}, { right: "<svg />" });
|
|
113
110
|
expect(wrapper.find(".link-text__icon--left").exists()).toBe(false);
|
|
114
111
|
expect(wrapper.find(".link-text__icon--right").exists()).toBe(true);
|
|
115
112
|
});
|
|
116
113
|
|
|
117
|
-
it("renders
|
|
118
|
-
const wrapper = await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
it("renders left icon before label and right icon after label", async () => {
|
|
115
|
+
const wrapper = await mountLink({}, { left: "<span class='l' />", right: "<span class='r' />" });
|
|
116
|
+
const children = Array.from((wrapper.element as HTMLElement).children);
|
|
117
|
+
expect(children.map((child) => child.classList[1] ?? child.classList[0])).toEqual([
|
|
118
|
+
"link-text__icon--left",
|
|
119
|
+
"link-text__label",
|
|
120
|
+
"link-text__icon--right",
|
|
121
|
+
]);
|
|
124
122
|
});
|
|
125
123
|
|
|
126
|
-
// ───
|
|
124
|
+
// ─── Link attributes ─────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
it("renders the to prop as href", async () => {
|
|
127
|
+
const wrapper = await mountLink({ to: "/services" });
|
|
128
|
+
expect(wrapper.attributes("href")).toBe("/services");
|
|
129
|
+
});
|
|
127
130
|
|
|
128
131
|
it("passes target prop to NuxtLink", async () => {
|
|
132
|
+
const wrapper = await mountLink({ to: "https://example.com", target: "_blank" });
|
|
133
|
+
expect(wrapper.attributes("target")).toBe("_blank");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("adds rel noopener to an external link opening in a new tab", async () => {
|
|
137
|
+
const wrapper = await mountLink({ to: "https://example.com", external: true, target: "_blank" });
|
|
138
|
+
expect(wrapper.attributes("rel")).toContain("noopener");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("forwards an aria-label to the link for an icon-only link", async () => {
|
|
129
142
|
const wrapper = await mountSuspended(LinkText, {
|
|
130
|
-
props: { to: "
|
|
143
|
+
props: { to: "/", linkText: "" },
|
|
144
|
+
attrs: { "aria-label": "Home" },
|
|
145
|
+
slots: { left: "<svg />" },
|
|
131
146
|
});
|
|
132
|
-
|
|
147
|
+
mounted.push(wrapper);
|
|
148
|
+
expect(wrapper.attributes("aria-label")).toBe("Home");
|
|
133
149
|
});
|
|
134
150
|
|
|
135
151
|
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
136
152
|
|
|
137
153
|
it("applies a styleClassPassthrough array", async () => {
|
|
138
|
-
const wrapper = await
|
|
139
|
-
props: {
|
|
140
|
-
to: "/",
|
|
141
|
-
linkText: "Styled",
|
|
142
|
-
styleClassPassthrough: ["class-a", "class-b"],
|
|
143
|
-
},
|
|
144
|
-
});
|
|
154
|
+
const wrapper = await mountLink({ styleClassPassthrough: ["class-a", "class-b"] });
|
|
145
155
|
expect(wrapper.classes()).toContain("class-a");
|
|
146
156
|
expect(wrapper.classes()).toContain("class-b");
|
|
147
157
|
});
|
|
148
158
|
|
|
149
159
|
it("applies a single styleClassPassthrough string", async () => {
|
|
150
|
-
const wrapper = await
|
|
151
|
-
props: { to: "/", linkText: "Styled", styleClassPassthrough: "solo-class" },
|
|
152
|
-
});
|
|
160
|
+
const wrapper = await mountLink({ styleClassPassthrough: "solo-class" });
|
|
153
161
|
expect(wrapper.classes()).toContain("solo-class");
|
|
154
162
|
});
|
|
155
163
|
|
|
156
|
-
// ─── DOM order ───────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
it("renders left icon before label and right icon after label", async () => {
|
|
159
|
-
const wrapper = await mountSuspended(LinkText, {
|
|
160
|
-
props: { to: "/", linkText: "Ordered" },
|
|
161
|
-
slots: { left: "<span class='l' />", right: "<span class='r' />" },
|
|
162
|
-
});
|
|
163
|
-
const children = wrapper.element.children;
|
|
164
|
-
expect(children[0].classList.contains("link-text__icon--left")).toBe(true);
|
|
165
|
-
expect(children[1].classList.contains("link-text__label")).toBe(true);
|
|
166
|
-
expect(children[2].classList.contains("link-text__icon--right")).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe("LinkText styleClassPassthrough reactivity", () => {
|
|
171
164
|
it("updates classes when styleClassPassthrough changes after mount", async () => {
|
|
172
|
-
const wrapper = await
|
|
173
|
-
props: { to: "/about", linkText: "About", styleClassPassthrough: ["original"] },
|
|
174
|
-
});
|
|
165
|
+
const wrapper = await mountLink({ styleClassPassthrough: ["original"] });
|
|
175
166
|
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
176
167
|
expect(wrapper.classes()).not.toContain("original");
|
|
177
168
|
expect(wrapper.classes()).toContain("updated");
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
exports[`PriceList > Snapshots > custom headings 1`] = `
|
|
4
4
|
"<div class="price-list">
|
|
5
5
|
<div class="price-list__column">
|
|
6
|
-
<h2 class="hero-text price-list__heading subheading axis
|
|
7
|
-
<!--v-if--><span class="text-
|
|
6
|
+
<h2 class="hero-text price-list__heading" data-font-size="subheading" data-axis="horizontal">
|
|
7
|
+
<!--v-if--><span class="hero-text-segment">Cuts</span>
|
|
8
8
|
</h2>
|
|
9
9
|
<dl class="price-list__list"></dl>
|
|
10
10
|
</div>
|
|
11
11
|
<div class="price-list__column">
|
|
12
|
-
<h2 class="hero-text price-list__heading subheading axis
|
|
13
|
-
<!--v-if--><span class="text-
|
|
12
|
+
<h2 class="hero-text price-list__heading" data-font-size="subheading" data-axis="horizontal">
|
|
13
|
+
<!--v-if--><span class="hero-text-segment">Colour</span>
|
|
14
14
|
</h2>
|
|
15
15
|
<dl class="price-list__list"></dl>
|
|
16
16
|
</div>
|
|
@@ -20,8 +20,8 @@ exports[`PriceList > Snapshots > custom headings 1`] = `
|
|
|
20
20
|
exports[`PriceList > Snapshots > default 1`] = `
|
|
21
21
|
"<div class="price-list">
|
|
22
22
|
<div class="price-list__column">
|
|
23
|
-
<h2 class="hero-text price-list__heading subheading axis
|
|
24
|
-
<!--v-if--><span class="text-
|
|
23
|
+
<h2 class="hero-text price-list__heading" data-font-size="subheading" data-axis="horizontal">
|
|
24
|
+
<!--v-if--><span class="hero-text-segment">Cutting & Treatment</span>
|
|
25
25
|
</h2>
|
|
26
26
|
<dl class="price-list__list">
|
|
27
27
|
<div class="price-list__row">
|
|
@@ -39,8 +39,8 @@ exports[`PriceList > Snapshots > default 1`] = `
|
|
|
39
39
|
</dl>
|
|
40
40
|
</div>
|
|
41
41
|
<div class="price-list__column">
|
|
42
|
-
<h2 class="hero-text price-list__heading subheading axis
|
|
43
|
-
<!--v-if--><span class="text-
|
|
42
|
+
<h2 class="hero-text price-list__heading" data-font-size="subheading" data-axis="horizontal">
|
|
43
|
+
<!--v-if--><span class="hero-text-segment">Hair Colouring</span>
|
|
44
44
|
</h2>
|
|
45
45
|
<dl class="price-list__list">
|
|
46
46
|
<div class="price-list__row">
|
|
@@ -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 ──────────────────────────────────────────────
|