@eox/pages-theme-eox 0.11.4 → 1.0.0
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/.gitlab-ci.yml +30 -0
- package/.release-it.json +15 -0
- package/CHANGELOG.md +8 -0
- package/README.md +235 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/component-index.html +11 -0
- package/cypress/support/component.js +22 -0
- package/cypress/support/mocks/features.data.js +9 -0
- package/cypress/support/mocks/helpers.js +48 -0
- package/cypress/support/mocks/vitepress.js +83 -0
- package/cypress.config.js +46 -0
- package/package.json +12 -2
- package/src/components/CTASection.cy.js +72 -0
- package/src/components/CookieBanner.cy.js +49 -0
- package/src/components/CookieSettings.cy.js +73 -0
- package/src/components/DataTable.cy.js +58 -0
- package/src/components/FeatureCard.cy.js +83 -0
- package/src/components/FeatureCard.vue +13 -6
- package/src/components/FeatureSection.cy.js +63 -0
- package/src/components/FeaturesGallery.cy.js +45 -0
- package/src/components/FeaturesGallery.vue +6 -3
- package/src/components/Footer.cy.js +52 -0
- package/src/components/Footer.vue +26 -7
- package/src/components/HeroSection.cy.js +66 -0
- package/src/components/LogoSection.cy.js +53 -0
- package/src/components/MobileNavDropdown.cy.js +67 -0
- package/src/components/MobileNavDropdown.vue +47 -0
- package/src/components/NavBar.cy.js +106 -0
- package/src/components/NavBar.vue +55 -3
- package/src/components/NavDropdown.cy.js +71 -0
- package/src/components/NavDropdown.vue +61 -0
- package/src/components/NewsBanner.cy.js +23 -0
- package/src/components/NewsBanner.vue +0 -1
- package/src/components/NotFound.cy.js +16 -0
- package/src/components/PricingTable.cy.js +91 -0
- package/src/helpers.js +53 -0
- package/src/index.js +1 -0
- package/src/style.css +121 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import CookieBanner from "./CookieBanner.vue";
|
|
2
|
+
import { __setMockData } from "../../cypress/support/mocks/vitepress";
|
|
3
|
+
import * as helpers from "../../cypress/support/mocks/helpers";
|
|
4
|
+
|
|
5
|
+
describe("<CookieBanner />", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Spy on helper functions to verify interactions
|
|
8
|
+
// Requires mock that delegates to window.__helpersMock
|
|
9
|
+
cy.spy(window.__helpersMock, "acceptCookies").as("acceptCookies");
|
|
10
|
+
cy.spy(window.__helpersMock, "declineCookies").as("declineCookies");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders with privacy link", () => {
|
|
14
|
+
const policyLink = "/privacy-policy";
|
|
15
|
+
__setMockData({
|
|
16
|
+
theme: {
|
|
17
|
+
theme: {
|
|
18
|
+
brandConfig: {
|
|
19
|
+
legal: {
|
|
20
|
+
privacyPolicy: policyLink,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
cy.mount(CookieBanner);
|
|
28
|
+
|
|
29
|
+
cy.contains("We use optional cookies").should("be.visible");
|
|
30
|
+
cy.contains("a", "privacy policy").should("have.attr", "href", policyLink);
|
|
31
|
+
cy.contains("a", "manage cookies").should(
|
|
32
|
+
"have.attr",
|
|
33
|
+
"href",
|
|
34
|
+
"/cookie-settings",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("accepts cookies on click", () => {
|
|
39
|
+
cy.mount(CookieBanner);
|
|
40
|
+
cy.contains("button", "Accept all").click();
|
|
41
|
+
cy.get("@acceptCookies").should("have.been.called");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("declines cookies on click", () => {
|
|
45
|
+
cy.mount(CookieBanner);
|
|
46
|
+
cy.contains("button", "Reject all").click();
|
|
47
|
+
cy.get("@declineCookies").should("have.been.called");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import CookieSettings from "./CookieSettings.vue";
|
|
2
|
+
import {
|
|
3
|
+
__setMockData,
|
|
4
|
+
__setRouteMock,
|
|
5
|
+
} from "../../cypress/support/mocks/vitepress";
|
|
6
|
+
import * as helpers from "../../cypress/support/mocks/helpers";
|
|
7
|
+
|
|
8
|
+
describe("<CookieSettings />", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Reset mocks
|
|
11
|
+
cy.spy(window.__helpersMock, "enableTracking").as("enableTracking");
|
|
12
|
+
cy.spy(window.__helpersMock, "showBanner").as("showBanner");
|
|
13
|
+
|
|
14
|
+
// Mock global _paq if strictly necessary, but helpers are mocked so logic inside component that calls _paq directly needs attention.
|
|
15
|
+
// The component calls _paq directly in the watcher.
|
|
16
|
+
// We can mock window._paq
|
|
17
|
+
cy.window().then((win) => {
|
|
18
|
+
win._paq = [];
|
|
19
|
+
cy.spy(win._paq, "push").as("paqPush");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
__setMockData({
|
|
23
|
+
theme: {
|
|
24
|
+
theme: {
|
|
25
|
+
brandConfig: {
|
|
26
|
+
legal: {
|
|
27
|
+
privacyPolicy: "/privacy",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Mock route data structure expected by component
|
|
35
|
+
__setRouteMock({
|
|
36
|
+
data: { title: "" },
|
|
37
|
+
onBeforeRouteChange: () => {},
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders cookie categories", () => {
|
|
42
|
+
cy.mount(CookieSettings);
|
|
43
|
+
|
|
44
|
+
cy.contains("Cookie Settings").should("be.visible");
|
|
45
|
+
cy.contains("h6", "Essential").should("be.visible");
|
|
46
|
+
cy.contains("h6", "Analytics").should("be.visible");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders privacy policy link", () => {
|
|
50
|
+
cy.mount(CookieSettings);
|
|
51
|
+
cy.contains("a", "Privacy Policy").should("have.attr", "href", "/privacy");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("toggles analytics cookies", () => {
|
|
55
|
+
cy.mount(CookieSettings);
|
|
56
|
+
|
|
57
|
+
// Analytics checkbox should be unchecked by default (if no cookie present)
|
|
58
|
+
cy.get('#Analytics input[type="checkbox"]').should("not.be.checked");
|
|
59
|
+
|
|
60
|
+
// Check it
|
|
61
|
+
cy.get('#Analytics input[type="checkbox"]').check({ force: true }); // force because of custom styling potentially hiding input
|
|
62
|
+
|
|
63
|
+
// verify _paq interaction
|
|
64
|
+
cy.get("@paqPush").should("have.been.calledWith", ["forgetUserOptOut"]);
|
|
65
|
+
cy.get("@enableTracking").should("have.been.calledWith", true); // second arg router is object
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("disables essential cookies checkbox", () => {
|
|
69
|
+
cy.mount(CookieSettings);
|
|
70
|
+
cy.get('#Essential input[type="checkbox"]').should("be.checked");
|
|
71
|
+
cy.get('#Essential input[type="checkbox"]').should("be.disabled");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import DataTable from "./DataTable.vue";
|
|
2
|
+
|
|
3
|
+
describe("<DataTable />", () => {
|
|
4
|
+
const headers = ["Name", "Description"];
|
|
5
|
+
const data = [
|
|
6
|
+
{
|
|
7
|
+
summary: { Name: "Item 1", Description: "Desc 1" },
|
|
8
|
+
content: "Detailed content for Item 1",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
summary: { Name: "Item 2", Description: "Desc 2" },
|
|
12
|
+
content: "Detailed content for Item 2",
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
it("renders headers correctly", () => {
|
|
17
|
+
cy.mount(DataTable, {
|
|
18
|
+
props: { headers, data },
|
|
19
|
+
});
|
|
20
|
+
headers.forEach((h) => cy.contains("th", h).should("be.visible"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders summary data in rows", () => {
|
|
24
|
+
cy.mount(DataTable, {
|
|
25
|
+
props: { headers, data },
|
|
26
|
+
});
|
|
27
|
+
cy.contains("Item 1").should("be.visible");
|
|
28
|
+
cy.contains("Desc 1").should("be.visible");
|
|
29
|
+
cy.contains("Item 2").should("be.visible");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("expands row to show detail content on click", () => {
|
|
33
|
+
cy.mount(DataTable, {
|
|
34
|
+
props: { headers, data },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Content should not be visible initially
|
|
38
|
+
cy.contains("Detailed content for Item 1").should("not.exist");
|
|
39
|
+
|
|
40
|
+
// Click the first row (the summary part)
|
|
41
|
+
cy.contains("Item 1").parents("tr").click();
|
|
42
|
+
|
|
43
|
+
// Content should now be visible
|
|
44
|
+
cy.contains("Detailed content for Item 1").should("be.visible");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("collapses row on second click", () => {
|
|
48
|
+
cy.mount(DataTable, {
|
|
49
|
+
props: { headers, data },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
cy.contains("Item 1").parents("tr").click();
|
|
53
|
+
cy.contains("Detailed content for Item 1").should("be.visible");
|
|
54
|
+
|
|
55
|
+
cy.contains("Item 1").parents("tr").click();
|
|
56
|
+
cy.contains("Detailed content for Item 1").should("not.exist");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import FeatureCard from "./FeatureCard.vue";
|
|
2
|
+
|
|
3
|
+
describe("<FeatureCard />", () => {
|
|
4
|
+
it("renders title and content using props", () => {
|
|
5
|
+
const title = "Test Feature";
|
|
6
|
+
const content = "This is a test feature content.";
|
|
7
|
+
|
|
8
|
+
cy.mount(FeatureCard, {
|
|
9
|
+
props: {
|
|
10
|
+
title,
|
|
11
|
+
content,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
cy.get("h5").should("contain", title);
|
|
16
|
+
// When using content prop without slots, it renders a div with v-html
|
|
17
|
+
cy.contains(content).should("exist");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders content in a paragraph when using slots", () => {
|
|
21
|
+
const title = "Slot Feature";
|
|
22
|
+
const slotContent = "This is content from a slot.";
|
|
23
|
+
|
|
24
|
+
cy.mount(FeatureCard, {
|
|
25
|
+
props: {
|
|
26
|
+
title,
|
|
27
|
+
},
|
|
28
|
+
slots: {
|
|
29
|
+
default: slotContent,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
cy.get("h5").should("contain", title);
|
|
34
|
+
// When using slots, it renders a p tag
|
|
35
|
+
cy.get("p").should("contain", slotContent);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders link when provided", () => {
|
|
39
|
+
const link = {
|
|
40
|
+
text: "Learn More",
|
|
41
|
+
href: "https://example.com",
|
|
42
|
+
target: "_blank",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
cy.mount(FeatureCard, {
|
|
46
|
+
props: {
|
|
47
|
+
title: "With Link",
|
|
48
|
+
content: "Content",
|
|
49
|
+
link,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
cy.get("a").should("have.attr", "href", "https://example.com");
|
|
54
|
+
cy.get("a").should("contain", "Learn More");
|
|
55
|
+
cy.get("a").should("have.attr", "target", "_blank");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders icon from class string", () => {
|
|
59
|
+
cy.mount(FeatureCard, {
|
|
60
|
+
props: {
|
|
61
|
+
title: "With Icon",
|
|
62
|
+
content: "Content",
|
|
63
|
+
icon: "mdi-home",
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
cy.get("i.mdi.mdi-home").should("exist");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders html icon", () => {
|
|
71
|
+
const iconHtml = '<svg><circle cx="50" cy="50" r="40" /></svg>';
|
|
72
|
+
cy.mount(FeatureCard, {
|
|
73
|
+
props: {
|
|
74
|
+
title: "With SVG Icon",
|
|
75
|
+
content: "Content",
|
|
76
|
+
icon: { html: iconHtml },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
cy.get(".icon").should("exist");
|
|
81
|
+
cy.get(".icon").find("svg").should("exist");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -21,16 +21,23 @@ const iconStyle = {
|
|
|
21
21
|
<template>
|
|
22
22
|
<article class="vertical large-padding">
|
|
23
23
|
<div>
|
|
24
|
-
<i
|
|
25
|
-
|
|
24
|
+
<i
|
|
25
|
+
v-if="typeof icon === 'string' && icon.startsWith('mdi-')"
|
|
26
|
+
:class="`mdi ${icon}`"
|
|
27
|
+
></i>
|
|
28
|
+
<div
|
|
29
|
+
v-else-if="icon"
|
|
30
|
+
:style="iconStyle"
|
|
31
|
+
v-html="icon.html"
|
|
32
|
+
class="icon"
|
|
33
|
+
></div>
|
|
26
34
|
<h5 class="small">{{ title }}</h5>
|
|
27
|
-
<p>
|
|
35
|
+
<p v-if="$slots.default">
|
|
28
36
|
<slot>{{ content }}</slot>
|
|
29
37
|
</p>
|
|
38
|
+
<div v-else v-html="content"></div>
|
|
30
39
|
</div>
|
|
31
|
-
<nav
|
|
32
|
-
v-if="link"
|
|
33
|
-
>
|
|
40
|
+
<nav v-if="link">
|
|
34
41
|
<a
|
|
35
42
|
:href="link.href"
|
|
36
43
|
:target="link.target ?? '_blank'"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import FeatureSection from "./FeatureSection.vue";
|
|
2
|
+
|
|
3
|
+
describe("<FeatureSection />", () => {
|
|
4
|
+
const defaultProps = {
|
|
5
|
+
title: "Feature Title",
|
|
6
|
+
tagline: "Feature Tagline",
|
|
7
|
+
icon: "mdi-star",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
it("renders title, tagline and icon", () => {
|
|
11
|
+
cy.mount(FeatureSection, {
|
|
12
|
+
props: defaultProps,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
cy.contains(defaultProps.title).should("exist");
|
|
16
|
+
cy.contains(defaultProps.tagline).should("exist");
|
|
17
|
+
cy.get(".mdi-star").should("exist");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders slot content", () => {
|
|
21
|
+
cy.mount(FeatureSection, {
|
|
22
|
+
props: defaultProps,
|
|
23
|
+
slots: {
|
|
24
|
+
default: "This is the feature content",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
cy.contains("This is the feature content").should("exist");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders buttons when links are provided", () => {
|
|
32
|
+
cy.mount(FeatureSection, {
|
|
33
|
+
props: {
|
|
34
|
+
...defaultProps,
|
|
35
|
+
primaryLink: "/primary",
|
|
36
|
+
primaryButton: "Primary Btn",
|
|
37
|
+
secondaryLink: "/secondary",
|
|
38
|
+
secondaryButton: "Secondary Btn",
|
|
39
|
+
altLink: "/alt",
|
|
40
|
+
altButton: "Alt Btn",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cy.contains("a", "Primary Btn").should("have.attr", "href", "/primary");
|
|
45
|
+
cy.contains("a", "Secondary Btn").should("have.attr", "href", "/secondary");
|
|
46
|
+
cy.contains("a", "Alt Btn").should("have.attr", "href", "/alt");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("applies styling classes based on props", () => {
|
|
50
|
+
cy.mount(FeatureSection, {
|
|
51
|
+
props: {
|
|
52
|
+
...defaultProps,
|
|
53
|
+
landing: true,
|
|
54
|
+
dark: true,
|
|
55
|
+
reverse: true,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cy.get(".feature-section").should("have.class", "landing");
|
|
60
|
+
cy.get(".feature-section").should("have.class", "dark");
|
|
61
|
+
cy.get(".feature-section").should("have.class", "reverse");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import FeaturesGallery from "./FeaturesGallery.vue";
|
|
2
|
+
import { __setMockData } from "../../cypress/support/mocks/vitepress";
|
|
3
|
+
|
|
4
|
+
describe("<FeaturesGallery />", () => {
|
|
5
|
+
const cards = [
|
|
6
|
+
{ id: 1, title: "Card 1", content: "Content 1", icon: "mdi-home" },
|
|
7
|
+
{ id: 2, title: "Card 2", content: "Content 2" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
__setMockData({});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("renders section title", () => {
|
|
15
|
+
cy.mount(FeaturesGallery, {
|
|
16
|
+
props: {
|
|
17
|
+
sectionTitle: "My Gallery",
|
|
18
|
+
cards,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
cy.contains("h5", "My Gallery").should("be.visible");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders feature cards", () => {
|
|
26
|
+
cy.mount(FeaturesGallery, {
|
|
27
|
+
props: { cards },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
cy.contains("Card 1").should("exist");
|
|
31
|
+
cy.contains("Content 1").should("exist");
|
|
32
|
+
cy.contains("Card 2").should("exist");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("applies background classes", () => {
|
|
36
|
+
cy.mount(FeaturesGallery, {
|
|
37
|
+
props: {
|
|
38
|
+
cards,
|
|
39
|
+
background: "custom-bg-class",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
cy.get("section").should("have.class", "custom-bg-class");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -69,7 +69,10 @@ const featuresExcerpts =
|
|
|
69
69
|
);
|
|
70
70
|
})
|
|
71
71
|
.filter((f) => f);
|
|
72
|
-
const siteTitle =
|
|
72
|
+
const siteTitle =
|
|
73
|
+
sectionTitle !== false
|
|
74
|
+
? sectionTitle || `More ${site.value.title} features:`
|
|
75
|
+
: false;
|
|
73
76
|
</script>
|
|
74
77
|
|
|
75
78
|
<template>
|
|
@@ -126,11 +129,11 @@ section {
|
|
|
126
129
|
.link-card {
|
|
127
130
|
display: block;
|
|
128
131
|
text-decoration: none;
|
|
129
|
-
border: .0625rem solid transparent;
|
|
132
|
+
border: 0.0625rem solid transparent;
|
|
130
133
|
}
|
|
131
134
|
.link-card:hover article {
|
|
132
135
|
box-shadow: none;
|
|
133
|
-
border: .0625rem solid var(--outline-variant);
|
|
136
|
+
border: 0.0625rem solid var(--outline-variant);
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
.card-wrapper {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Footer from "./Footer.vue";
|
|
2
|
+
import { __setMockData } from "../../cypress/support/mocks/vitepress";
|
|
3
|
+
|
|
4
|
+
describe("<Footer />", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
__setMockData({
|
|
7
|
+
site: { title: "EOX Site" },
|
|
8
|
+
theme: {
|
|
9
|
+
logo: { light: "/logo.png" },
|
|
10
|
+
nav: [
|
|
11
|
+
{ text: "Link 1", link: "/link1" },
|
|
12
|
+
{ text: "Contact", link: "/contact" },
|
|
13
|
+
],
|
|
14
|
+
footer: {
|
|
15
|
+
copyright: "© 2026 EOX",
|
|
16
|
+
},
|
|
17
|
+
theme: {
|
|
18
|
+
brandConfig: {
|
|
19
|
+
legal: {
|
|
20
|
+
about: "/about",
|
|
21
|
+
termsAndConditions: "/terms",
|
|
22
|
+
privacyPolicy: "/privacy",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders logo and copyright", () => {
|
|
31
|
+
cy.mount(Footer);
|
|
32
|
+
|
|
33
|
+
cy.get("img.logo").should("have.attr", "src", "/logo.png");
|
|
34
|
+
cy.contains("© 2026 EOX").should("exist");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders contact button", () => {
|
|
38
|
+
cy.mount(Footer);
|
|
39
|
+
cy.contains("a", "Contact").should("have.attr", "href", "/contact");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders legal links", () => {
|
|
43
|
+
cy.mount(Footer);
|
|
44
|
+
cy.contains("a", "About").should("have.attr", "href", "/about");
|
|
45
|
+
cy.contains("a", "Terms & Conditions").should(
|
|
46
|
+
"have.attr",
|
|
47
|
+
"href",
|
|
48
|
+
"/terms",
|
|
49
|
+
);
|
|
50
|
+
cy.contains("a", "Privacy").should("have.attr", "href", "/privacy");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
/>
|
|
12
12
|
<div class="small-space"></div>
|
|
13
13
|
<a
|
|
14
|
-
v-if="theme.nav.find((i) => i.link.includes('contact'))"
|
|
15
|
-
:href="
|
|
14
|
+
v-if="theme.nav.find((i) => i.link && i.link.includes('contact'))"
|
|
15
|
+
:href="
|
|
16
|
+
theme.nav.find((i) => i.link && i.link.includes('contact')).link
|
|
17
|
+
"
|
|
16
18
|
class="button small border no-margin"
|
|
17
19
|
style="color: var(--on-surface)"
|
|
18
20
|
@click="trackEvent(['CTA', 'Click', 'Footer', action.text])"
|
|
19
|
-
>{{
|
|
21
|
+
>{{
|
|
22
|
+
theme.nav.find((i) => i.link && i.link.includes("contact")).text
|
|
23
|
+
}}</a
|
|
20
24
|
>
|
|
21
25
|
<p v-html="theme.footer.copyright"></p>
|
|
22
26
|
<p class="middle-align">
|
|
@@ -33,9 +37,12 @@
|
|
|
33
37
|
</div>
|
|
34
38
|
<div class="s12 l6">
|
|
35
39
|
<div class="grid large-line">
|
|
36
|
-
<div
|
|
40
|
+
<div
|
|
41
|
+
class="s6 l4"
|
|
42
|
+
v-if="theme.nav.filter((i) => !i.action && i.link).length"
|
|
43
|
+
>
|
|
37
44
|
<p class="bold">About</p>
|
|
38
|
-
<p v-for="item in theme.nav.filter((i) => !i.action)">
|
|
45
|
+
<p v-for="item in theme.nav.filter((i) => !i.action && i.link)">
|
|
39
46
|
<a
|
|
40
47
|
:href="withBase(item.link)"
|
|
41
48
|
:target="item.target"
|
|
@@ -45,7 +52,19 @@
|
|
|
45
52
|
>
|
|
46
53
|
</p>
|
|
47
54
|
</div>
|
|
48
|
-
<div class="s6">
|
|
55
|
+
<div class="s6 l4" v-for="item in theme.nav.filter((i) => i.items)">
|
|
56
|
+
<p class="bold">{{ item.text }}</p>
|
|
57
|
+
<p v-for="subItem in getFlatList(item.items)">
|
|
58
|
+
<a
|
|
59
|
+
:href="withBase(subItem.link)"
|
|
60
|
+
:target="subItem.target"
|
|
61
|
+
:rel="subItem.rel"
|
|
62
|
+
class="link"
|
|
63
|
+
>{{ subItem.text }}</a
|
|
64
|
+
>
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="s6 l4">
|
|
49
68
|
<p class="bold">Legal</p>
|
|
50
69
|
<p>
|
|
51
70
|
<a
|
|
@@ -94,7 +113,7 @@
|
|
|
94
113
|
|
|
95
114
|
<script setup>
|
|
96
115
|
import { useData, withBase } from "vitepress";
|
|
97
|
-
import { trackEvent } from "../helpers";
|
|
116
|
+
import { trackEvent, getFlatList } from "../helpers";
|
|
98
117
|
const { site, theme } = useData();
|
|
99
118
|
</script>
|
|
100
119
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import HeroSection from "./HeroSection.vue";
|
|
2
|
+
import { __setMockData } from "../../cypress/support/mocks/vitepress";
|
|
3
|
+
|
|
4
|
+
describe("<HeroSection />", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset mock data before each test
|
|
7
|
+
__setMockData({
|
|
8
|
+
frontmatter: {
|
|
9
|
+
hero: {
|
|
10
|
+
text: "Hero Text",
|
|
11
|
+
tagline: "Hero Tagline",
|
|
12
|
+
actions: [
|
|
13
|
+
{ text: "Start", link: "/start", theme: "brand" },
|
|
14
|
+
{ text: "More", link: "/more", theme: "secondary" },
|
|
15
|
+
],
|
|
16
|
+
image: { src: "/image.png", alt: "Hero Image" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders correctly with default data", () => {
|
|
23
|
+
cy.mount(HeroSection);
|
|
24
|
+
|
|
25
|
+
cy.get("h1").should("contain", "Hero Text");
|
|
26
|
+
cy.get("p").should("contain", "Hero Tagline");
|
|
27
|
+
|
|
28
|
+
// Check actions
|
|
29
|
+
cy.contains("a", "Start").should("have.attr", "href", "/start");
|
|
30
|
+
cy.contains("a", "Start").should("have.class", "primary-text"); // brand theme
|
|
31
|
+
|
|
32
|
+
cy.contains("a", "More").should("have.attr", "href", "/more");
|
|
33
|
+
cy.contains("a", "More").should("have.class", "secondary"); // secondary theme
|
|
34
|
+
|
|
35
|
+
// Check image
|
|
36
|
+
cy.get("img.hero-image").should("have.attr", "src", "/image.png");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders video when provided", () => {
|
|
40
|
+
__setMockData({
|
|
41
|
+
frontmatter: {
|
|
42
|
+
hero: {
|
|
43
|
+
text: "Video Hero",
|
|
44
|
+
video: { src: "/video.mp4" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
cy.mount(HeroSection);
|
|
50
|
+
cy.get("video.hero-image").should("have.attr", "src", "/video.mp4");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders background image", () => {
|
|
54
|
+
__setMockData({
|
|
55
|
+
frontmatter: {
|
|
56
|
+
hero: {
|
|
57
|
+
text: "BG Hero",
|
|
58
|
+
background: { src: "/bg.jpg" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
cy.mount(HeroSection);
|
|
64
|
+
cy.get("img.background-image").should("have.attr", "src", "/bg.jpg");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import LogoSection from "./LogoSection.vue";
|
|
2
|
+
|
|
3
|
+
describe("<LogoSection />", () => {
|
|
4
|
+
const logos = [
|
|
5
|
+
{ image: "/logo1.png", alt: "Logo 1", link: "https://example.com" },
|
|
6
|
+
{ image: "/logo2.png", alt: "Logo 2" }, // No link
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
it("renders list of logos", () => {
|
|
10
|
+
cy.mount(LogoSection, {
|
|
11
|
+
props: { logos },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
cy.get(".logo-row").find("img").should("have.length", 2);
|
|
15
|
+
cy.get('img[alt="Logo 1"]').should("exist");
|
|
16
|
+
cy.get('img[alt="Logo 2"]').should("exist");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("wraps logos with link when provided", () => {
|
|
20
|
+
cy.mount(LogoSection, {
|
|
21
|
+
props: { logos },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
cy.get('a[href="https://example.com"]')
|
|
25
|
+
.find('img[alt="Logo 1"]')
|
|
26
|
+
.should("exist");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders div instead of link when no link provided", () => {
|
|
30
|
+
cy.mount(LogoSection, {
|
|
31
|
+
props: { logos },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
cy.get("div.logo").find('img[alt="Logo 2"]').should("exist");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("sets css variables on mount", () => {
|
|
38
|
+
cy.mount(LogoSection, {
|
|
39
|
+
props: {
|
|
40
|
+
logos: [],
|
|
41
|
+
baseHeight: 5,
|
|
42
|
+
strength: 0.8,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
cy.get(".logo-row")
|
|
47
|
+
.should("have.attr", "style")
|
|
48
|
+
.and("include", "--base-height: 5rem");
|
|
49
|
+
cy.get(".logo-row")
|
|
50
|
+
.should("have.attr", "style")
|
|
51
|
+
.and("include", "--strength: 0.8");
|
|
52
|
+
});
|
|
53
|
+
});
|