@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.
Files changed (38) hide show
  1. package/.gitlab-ci.yml +30 -0
  2. package/.release-it.json +15 -0
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +235 -0
  5. package/cypress/support/commands.js +25 -0
  6. package/cypress/support/component-index.html +11 -0
  7. package/cypress/support/component.js +22 -0
  8. package/cypress/support/mocks/features.data.js +9 -0
  9. package/cypress/support/mocks/helpers.js +48 -0
  10. package/cypress/support/mocks/vitepress.js +83 -0
  11. package/cypress.config.js +46 -0
  12. package/package.json +12 -2
  13. package/src/components/CTASection.cy.js +72 -0
  14. package/src/components/CookieBanner.cy.js +49 -0
  15. package/src/components/CookieSettings.cy.js +73 -0
  16. package/src/components/DataTable.cy.js +58 -0
  17. package/src/components/FeatureCard.cy.js +83 -0
  18. package/src/components/FeatureCard.vue +13 -6
  19. package/src/components/FeatureSection.cy.js +63 -0
  20. package/src/components/FeaturesGallery.cy.js +45 -0
  21. package/src/components/FeaturesGallery.vue +6 -3
  22. package/src/components/Footer.cy.js +52 -0
  23. package/src/components/Footer.vue +26 -7
  24. package/src/components/HeroSection.cy.js +66 -0
  25. package/src/components/LogoSection.cy.js +53 -0
  26. package/src/components/MobileNavDropdown.cy.js +67 -0
  27. package/src/components/MobileNavDropdown.vue +47 -0
  28. package/src/components/NavBar.cy.js +106 -0
  29. package/src/components/NavBar.vue +55 -3
  30. package/src/components/NavDropdown.cy.js +71 -0
  31. package/src/components/NavDropdown.vue +61 -0
  32. package/src/components/NewsBanner.cy.js +23 -0
  33. package/src/components/NewsBanner.vue +0 -1
  34. package/src/components/NotFound.cy.js +16 -0
  35. package/src/components/PricingTable.cy.js +91 -0
  36. package/src/helpers.js +53 -0
  37. package/src/index.js +1 -0
  38. package/src/style.css +121 -4
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,30 @@
1
+ image: cypress/base:20.14.0
2
+
3
+ stages:
4
+ - test
5
+
6
+ cache:
7
+ key:
8
+ files:
9
+ - package-lock.json
10
+ paths:
11
+ - .npm/
12
+
13
+ variables:
14
+ npm_config_cache: "$CI_PROJECT_DIR/.npm"
15
+
16
+ format:
17
+ stage: test
18
+ script:
19
+ - npm ci
20
+ - npm run format:check
21
+ rules:
22
+ - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
23
+
24
+ test:
25
+ stage: test
26
+ script:
27
+ - npm ci
28
+ - npm run test:component
29
+ rules:
30
+ - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
@@ -0,0 +1,15 @@
1
+ {
2
+ "git": {
3
+ "commitMessage": "chore: release v${version}",
4
+ "tagName": "v${version}"
5
+ },
6
+ "npm": {
7
+ "publish": false
8
+ },
9
+ "plugins": {
10
+ "@release-it/conventional-changelog": {
11
+ "preset": "angular",
12
+ "infile": "CHANGELOG.md"
13
+ }
14
+ }
15
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ # [1.0.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v0.11.5...v1.0.0) (2026-01-20)
4
+
5
+ ### Features
6
+
7
+ - add support for TOC, socialLinks, sidebar and outline ([c512361](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/c51236122a65faf2edb6e8a575d5ef53d86c25af))
8
+ - support for (nested) dropdown nav entries ([71b531e](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/71b531e1193b18e1fa33397916d0b5a0d35df128))
package/README.md CHANGED
@@ -16,3 +16,238 @@ export default {
16
16
  ```
17
17
 
18
18
  See also the [Vitepress Docs](https://vitepress.dev/guide/custom-theme#consuming-a-custom-theme).
19
+
20
+ ## Configuration
21
+
22
+ This theme supports standard [VitePress Default Theme Configuration](https://vitepress.dev/reference/default-theme-config) with some specific adaptations.
23
+
24
+ ### Navigation Bar
25
+
26
+ The Navigation Bar follows the [Default Theme Nav](https://vitepress.dev/reference/default-theme-nav) structure with the following differences:
27
+
28
+ | Feature | Supported | Notes |
29
+ | :---------------- | :-------- | :------------------------------------------------------------------------------------------ |
30
+ | Links | ✅ | Standard `{ text, link }` structure. |
31
+ | Dropdowns | ✅ | Standard `{ text, items: [] }` structure, supports nesting. |
32
+ | Active Match | ✅ | `activeMatch` regex strings are supported. |
33
+ | Target/Rel | ✅ | `target` and `rel` attributes are supported. |
34
+ | Buttons | ✨ | **Custom**: Add `action: 'primary'` (or 'secondary') to a nav item to style it as a button. |
35
+ | Logo | ⚠️ | Must be an object with light/dark paths: `{ light: '...', dark: '...' }`. |
36
+ | Social Links | ✅ | Valid `socialLinks` will be displayed in the navbar. |
37
+ | Custom Components | ❌ | Embedding custom components directly in the nav array is not supported. |
38
+
39
+ #### Example Configuration
40
+
41
+ ```javascript
42
+ // .vitepress/config.js
43
+ export default {
44
+ themeConfig: {
45
+ logo: {
46
+ light: "/logo-light.svg",
47
+ dark: "/logo-dark.svg",
48
+ },
49
+ nav: [
50
+ { text: "Home", link: "/" },
51
+ {
52
+ text: "Products",
53
+ items: [
54
+ { text: "Product A", link: "/a" },
55
+ { text: "Product B", link: "/b" },
56
+ ],
57
+ },
58
+ // Custom generic button style
59
+ { text: "Contact Us", link: "/contact", action: "primary" },
60
+ ],
61
+ },
62
+ };
63
+ ```
64
+
65
+ ### General Configuration
66
+
67
+ The following standard VitePress configurations are handled as follows:
68
+
69
+ - **Site Title**: Explicitly disabled (`siteTitle: false`). The theme relies on the logo for branding.
70
+ - **Logo**: Used for branding. Must be configured as an object `{ light: string, dark: string }`.
71
+ - **Nav**: Fully supported (see above).
72
+ - **Footer**: Supported. The theme uses `theme.footer.copyright` for the copyright text.
73
+ - **Dark Mode**: The theme enforces a specific appearance. `appearance: false` is set in the base config.
74
+
75
+ **Note on Documentation Features**:
76
+ This theme is primarily designed for landing pages and product showcases.
77
+
78
+ - **Social Links** and **TOC** are supported as described in the VitePress documentation.
79
+ - **Sidebar** and **Outline** are automatically enabled for the `doc` layout, whereas the `page` layout doesn't have them.
80
+ - Other features like **Edit Link**, **Last Updated**, and **Algolia Search** are **not** currently integrated into the custom `Layout.vue` and may not function as expected if configured.
81
+
82
+ ### Cookie Consent
83
+
84
+ This theme includes built-in cookie consent management integrated with Matomo (Piwik) analytics.
85
+
86
+ - **Cookie Banner**: Automatically displayed if `brandConfig.analytics` is present.
87
+ - **Cookie Settings**: a `/cookie-settings` page is automatically handled to allow users to review and toggle optional cookies.
88
+
89
+ #### Analytics Configuration
90
+
91
+ To enable the cookie banner and analytics tracking, ensure your brand config includes:
92
+
93
+ ```javascript
94
+ // brandConfig
95
+ {
96
+ analytics: {
97
+ siteId: "YOUR_MATOMO_SITE_ID";
98
+ // The tracker URL is hardcoded to "https://nix.eox.at/piwik/" in vitepressConfig.mjs
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Layout & Components
104
+
105
+ The theme provides several built-in components and layout features. Some are driven by frontmatter configuration, while others can be used directly in your Markdown files.
106
+
107
+ ### Hero Section
108
+
109
+ To add a Hero section to the top of a page, use the `hero` frontmatter object.
110
+
111
+ ```yaml
112
+ ---
113
+ hero:
114
+ title: "Welcome to My Site"
115
+ tagline: "Building the future of things"
116
+ image:
117
+ src: "/hero-image.png"
118
+ alt: "Hero Image"
119
+ actions:
120
+ - text: "Get Started"
121
+ link: "/start"
122
+ type: "primary"
123
+ - text: "Learn More"
124
+ link: "/about"
125
+ type: "secondary"
126
+ ---
127
+ ```
128
+
129
+ ### Feature Section
130
+
131
+ Use the `<FeatureSection>` component to highlight specific features.
132
+
133
+ ```html
134
+ <FeatureSection
135
+ title="My Feature"
136
+ tagline="This is an amazing feature"
137
+ icon="mdi-star"
138
+ flipped
139
+ >
140
+ <template #default>
141
+ <p>Detailed description of the feature.</p>
142
+ </template>
143
+ <template #actions>
144
+ <a href="/docs" class="btn">Documentation</a>
145
+ </template>
146
+ </FeatureSection>
147
+ ```
148
+
149
+ ### Pricing Table
150
+
151
+ A flexible pricing table component.
152
+
153
+ ```html
154
+ <PricingTable
155
+ :config="{
156
+ plans: [
157
+ {
158
+ title: 'Basic',
159
+ price: '€0',
160
+ features: ['Feature 1', 'Feature 2'],
161
+ link: { text: 'Sign Up', href: '/signup' }
162
+ },
163
+ {
164
+ title: 'Pro',
165
+ price: '€99',
166
+ features: ['Everything in Basic', 'Feature 3'],
167
+ link: { text: 'Go Pro', href: '/pro' },
168
+ active: true
169
+ }
170
+ ]
171
+ }"
172
+ />
173
+ ```
174
+
175
+ ### Other Components
176
+
177
+ #### CTA Section
178
+
179
+ A Call-to-Action section typically placed at the bottom of a page to encourage user engagement.
180
+
181
+ ```html
182
+ <CTASection
183
+ title="Ready to get started?"
184
+ tagline="Join thousands of others today."
185
+ primary-button="Sign Up Now"
186
+ primary-link="/register"
187
+ secondary-button="Contact Sales"
188
+ secondary-link="/sales"
189
+ alt-button="Read Documentation"
190
+ alt-link="/docs"
191
+ dark
192
+ />
193
+ ```
194
+
195
+ #### Logo Section
196
+
197
+ Displays a grid of client or partner logos. It automatically handles logo sizing.
198
+
199
+ ```html
200
+ <LogoSection
201
+ :logos="[
202
+ { image: '/logos/client1.png', alt: 'Client 1', link: 'https://client1.com' },
203
+ { image: '/logos/client2.png', alt: 'Client 2' }
204
+ ]"
205
+ base-height="4"
206
+ strength="1"
207
+ />
208
+ ```
209
+
210
+ #### Data Table
211
+
212
+ An interactive table with expandable rows. The `summary` object keys must match the `headers` provided.
213
+
214
+ ```html
215
+ <DataTable
216
+ :headers="['Name', 'Type', 'Status']"
217
+ :data="[
218
+ {
219
+ summary: { Name: 'Task A', Type: 'Cron', Status: 'Active' },
220
+ content: 'Detailed description of Task A...'
221
+ },
222
+ {
223
+ summary: { Name: 'Task B', Type: 'Trigger', Status: 'Paused' },
224
+ content: 'Detailed description of Task B...'
225
+ }
226
+ ]"
227
+ />
228
+ ```
229
+
230
+ #### Features Gallery
231
+
232
+ A grid layout for feature cards. Automatically used on pages under `features/`, but can be used manually with custom cards.
233
+
234
+ ```html
235
+ <FeaturesGallery
236
+ section-title="All Features"
237
+ background="surface-container-low"
238
+ :cards="[
239
+ {
240
+ id: 1,
241
+ title: 'Global Coverage',
242
+ content: 'Access data from anywhere.',
243
+ icon: 'mdi-earth'
244
+ },
245
+ {
246
+ id: 2,
247
+ title: 'Real-time',
248
+ content: 'Updates as they happen.',
249
+ link: { text: 'Learn more', href: '/real-time' }
250
+ }
251
+ ]"
252
+ />
253
+ ```
@@ -0,0 +1,25 @@
1
+ // ***********************************************
2
+ // This example commands.js shows you how to
3
+ // create various custom commands and overwrite
4
+ // existing commands.
5
+ //
6
+ // For more comprehensive examples of custom
7
+ // commands please read more here:
8
+ // https://on.cypress.io/custom-commands
9
+ // ***********************************************
10
+ //
11
+ //
12
+ // -- This is a parent command --
13
+ // Cypress.Commands.add('login', (email, password) => { ... })
14
+ //
15
+ //
16
+ // -- This is a child command --
17
+ // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18
+ //
19
+ //
20
+ // -- This is a dual command --
21
+ // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22
+ //
23
+ //
24
+ // -- This will overwrite an existing command --
25
+ // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
6
+ <title>Components App</title>
7
+ </head>
8
+ <body>
9
+ <div data-cy-root></div>
10
+ </body>
11
+ </html>
@@ -0,0 +1,22 @@
1
+ // Import commands.js using ES2015 syntax:
2
+ import "./commands";
3
+
4
+ // Alternatively you can use CommonJS syntax:
5
+ // require('./commands')
6
+
7
+ import { mount } from "cypress/vue";
8
+
9
+ // Import global styles
10
+ import "../../src/style.css";
11
+
12
+ // Import EOX UI web components
13
+ import "@eox/ui";
14
+
15
+ // Ensure global styles are applied
16
+ // This might be needed if vite config doesn't pick up the CSS the same way
17
+ // But importing it here usually works for component testing
18
+
19
+ Cypress.Commands.add("mount", mount);
20
+
21
+ // Example use:
22
+ // cy.mount(MyComponent)
@@ -0,0 +1,9 @@
1
+ export const data = [
2
+ {
3
+ id: 1,
4
+ title: "Mock Feature 1",
5
+ content: "Content 1",
6
+ html: "<featuresection><h1>Mock Feature 1</h1><p>Content 1</p></featuresection>",
7
+ link: { text: "Read more", href: "#" },
8
+ },
9
+ ];
@@ -0,0 +1,48 @@
1
+ // Mock generic helpers
2
+ // We delegate to a global object to allow spying in tests
3
+ window.__helpersMock = {
4
+ trackEvent: (details) => {},
5
+ acceptCookies: (router) => {},
6
+ declineCookies: (router) => {},
7
+ enableTracking: (enable, router) => {},
8
+ showBanner: (show) => {
9
+ // Basic DOM simulation for banner visibility
10
+ if (typeof document !== "undefined") {
11
+ const banner = document.querySelector(".cookie-banner");
12
+ if (banner) {
13
+ banner.style.display = show ? "block" : "none";
14
+ }
15
+ }
16
+ },
17
+ isActive: (item, path) => {
18
+ if (item.link === path) return true;
19
+ if (item.items) {
20
+ return item.items.some((sub) => window.__helpersMock.isActive(sub, path));
21
+ }
22
+ return false;
23
+ },
24
+ getFlatList: (items) => {
25
+ let list = [];
26
+ items?.forEach((item) => {
27
+ if (item.link) {
28
+ list.push(item);
29
+ }
30
+ if (item.items) {
31
+ list = [...list, ...window.__helpersMock.getFlatList(item.items)];
32
+ }
33
+ });
34
+ return list;
35
+ },
36
+ };
37
+
38
+ export const trackEvent = (...args) => window.__helpersMock.trackEvent(...args);
39
+ export const acceptCookies = (...args) =>
40
+ window.__helpersMock.acceptCookies(...args);
41
+ export const declineCookies = (...args) =>
42
+ window.__helpersMock.declineCookies(...args);
43
+ export const enableTracking = (...args) =>
44
+ window.__helpersMock.enableTracking(...args);
45
+ export const showBanner = (...args) => window.__helpersMock.showBanner(...args);
46
+ export const isActive = (...args) => window.__helpersMock.isActive(...args);
47
+ export const getFlatList = (...args) =>
48
+ window.__helpersMock.getFlatList(...args);
@@ -0,0 +1,83 @@
1
+ import { reactive, ref } from "vue";
2
+
3
+ const site = ref({
4
+ title: "Test Site",
5
+ description: "Test Description",
6
+ });
7
+
8
+ const page = ref({
9
+ relativePath: "index.md",
10
+ title: "Test Page",
11
+ frontmatter: {},
12
+ });
13
+
14
+ const theme = ref({
15
+ logo: { light: "/logo.png", dark: "/logo-dark.png" },
16
+ nav: [],
17
+ footer: { copyright: "Test Copyright" },
18
+ theme: {
19
+ brandConfig: {},
20
+ },
21
+ });
22
+
23
+ const frontmatter = ref({});
24
+
25
+ export function useData() {
26
+ return {
27
+ site,
28
+ page,
29
+ theme,
30
+ frontmatter,
31
+ lang: ref("en-US"),
32
+ localeIndex: ref("root"),
33
+ };
34
+ }
35
+
36
+ export function withBase(path) {
37
+ return path;
38
+ }
39
+
40
+ // Helper to update mock data from tests
41
+ export function __setMockData(newData) {
42
+ if (newData.site) site.value = { ...site.value, ...newData.site };
43
+ if (newData.page) page.value = { ...page.value, ...newData.page };
44
+ if (newData.theme) theme.value = { ...theme.value, ...newData.theme };
45
+ if (newData.frontmatter)
46
+ frontmatter.value = { ...frontmatter.value, ...newData.frontmatter };
47
+ }
48
+
49
+ // Mock route
50
+ const route = reactive({
51
+ path: "/",
52
+ data: {
53
+ title: "Test Page",
54
+ relativePath: "index.md",
55
+ },
56
+ });
57
+
58
+ export function useRoute() {
59
+ return route;
60
+ }
61
+
62
+ // Mock useRouter
63
+ export function useRouter() {
64
+ return {
65
+ route,
66
+ go: () => {},
67
+ // Mock onBeforeRouteChange hook registry
68
+ onBeforeRouteChange: (fn) => {
69
+ // We can expose this to tests if needed to trigger it manually
70
+ window.__mockOnBeforeRouteChange = fn;
71
+ },
72
+ };
73
+ }
74
+
75
+ export function createContentLoader() {
76
+ return {
77
+ load: () => Promise.resolve([]),
78
+ };
79
+ }
80
+
81
+ export function __setRouteMock(newRoute) {
82
+ Object.assign(route, newRoute);
83
+ }
@@ -0,0 +1,46 @@
1
+ import { defineConfig } from "cypress";
2
+ import vue from "@vitejs/plugin-vue";
3
+ import path from "path";
4
+
5
+ export default defineConfig({
6
+ component: {
7
+ devServer: {
8
+ framework: "vue",
9
+ bundler: "vite",
10
+ viteConfig: {
11
+ resolve: {
12
+ alias: {
13
+ // Alias vitepress to our mock
14
+ vitepress: path.resolve(
15
+ __dirname,
16
+ "cypress/support/mocks/vitepress.js",
17
+ ),
18
+ // Alias helpers to our mock
19
+ "../helpers": path.resolve(
20
+ __dirname,
21
+ "cypress/support/mocks/helpers.js",
22
+ ),
23
+ "../helpers.js": path.resolve(
24
+ __dirname,
25
+ "cypress/support/mocks/helpers.js",
26
+ ),
27
+ // Alias features.data.js to our mock
28
+ "../features.data.js": path.resolve(
29
+ __dirname,
30
+ "cypress/support/mocks/features.data.js",
31
+ ),
32
+ },
33
+ },
34
+ plugins: [
35
+ vue({
36
+ template: {
37
+ compilerOptions: {
38
+ isCustomElement: (tag) => tag.includes("-"),
39
+ },
40
+ },
41
+ }),
42
+ ],
43
+ },
44
+ },
45
+ },
46
+ });
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "0.11.4",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
7
7
  "author": "EOX",
8
8
  "license": "MIT",
9
9
  "scripts": {
10
- "format": "npx prettier --write ."
10
+ "format": "npx prettier --write .",
11
+ "format:check": "npx prettier --check .",
12
+ "test:component": "cypress run --component",
13
+ "release": "release-it"
11
14
  },
12
15
  "dependencies": {
13
16
  "@eox/eslint-config": "^2.0.0",
14
17
  "@eox/ui": "^0.3.7",
15
18
  "vitepress": "^1.6.3"
19
+ },
20
+ "devDependencies": {
21
+ "@release-it/conventional-changelog": "^10.0.4",
22
+ "@vitejs/plugin-vue": "^6.0.3",
23
+ "cypress": "^15.9.0",
24
+ "release-it": "^19.2.3",
25
+ "vue": "^3.5.26"
16
26
  }
17
27
  }
@@ -0,0 +1,72 @@
1
+ import CTASection from "./CTASection.vue";
2
+
3
+ describe("<CTASection />", () => {
4
+ it("renders title and tagline", () => {
5
+ const title = "Call to Action";
6
+ const tagline = "Do it now!";
7
+
8
+ cy.mount(CTASection, {
9
+ props: {
10
+ title,
11
+ tagline,
12
+ },
13
+ });
14
+
15
+ cy.get("h3").should("contain", title);
16
+ cy.get("p.large-text").should("contain", tagline);
17
+ });
18
+
19
+ it("renders buttons correctly", () => {
20
+ const primaryButton = "Primary";
21
+ const primaryLink = "/primary";
22
+ const secondaryButton = "Secondary";
23
+ const secondaryLink = "/secondary";
24
+ const altButton = "Alt";
25
+ const altLink = "/alt";
26
+
27
+ cy.mount(CTASection, {
28
+ props: {
29
+ title: "Title",
30
+ tagline: "Tagline",
31
+ primaryButton,
32
+ primaryLink,
33
+ secondaryButton,
34
+ secondaryLink,
35
+ altButton,
36
+ altLink,
37
+ },
38
+ });
39
+
40
+ cy.contains("a", primaryButton).should("have.attr", "href", primaryLink);
41
+ // Primary button should have primary class unless dark mode (which adds surface class and removes primary)
42
+ // By default dark is undefined
43
+ cy.contains("a", primaryButton).should("have.class", "primary");
44
+
45
+ cy.contains("a", secondaryButton).should(
46
+ "have.attr",
47
+ "href",
48
+ secondaryLink,
49
+ );
50
+ cy.contains("a", secondaryButton).should("have.class", "secondary");
51
+
52
+ cy.contains("a", altButton).should("have.attr", "href", altLink);
53
+ cy.contains("a", altButton).should("have.class", "border");
54
+ });
55
+
56
+ it("applies dark mode styling", () => {
57
+ cy.mount(CTASection, {
58
+ props: {
59
+ title: "Dark Mode",
60
+ dark: true,
61
+ primaryButton: "Go",
62
+ },
63
+ });
64
+
65
+ cy.get(".cta-section").should("have.class", "primary");
66
+ cy.get(".cta-section").should("have.class", "primary-gradient-bg");
67
+
68
+ // Primary button in dark mode gets 'surface' class instead of 'primary'
69
+ cy.contains("a", "Go").should("have.class", "surface");
70
+ cy.contains("a", "Go").should("not.have.class", "primary");
71
+ });
72
+ });