Skip to content

Commit fa8acdb

Browse files
committed
add cypress ct
1 parent 3440285 commit fa8acdb

File tree

9 files changed

+242
-4
lines changed

9 files changed

+242
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
The [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) is a great API.
1212
But it may not be the one-size-fits-all solution to highlight menu/sidebar links.
1313

14-
You may noticed that clicking on some links highlights the wrong one (or does nothing) and that the active link may not reflect the one in the URL hash. But most important, you noticed that's tricky to obtain different behaviors according to different scroll interactions.
14+
You may noticed that clicking on some links highlights the wrong one (or does nothing) or that the active link doesn't reflect the one in the URL hash. But most important, you noticed that's tricky to obtain different behaviors according to different scroll interactions.
1515

1616
For example, you want to immediately highlight targets when scroll is originated from click but not when scroll is originated from wheel/touch.
1717

cypress.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from 'cypress';
2+
3+
export default defineConfig({
4+
component: {
5+
viewportWidth: 1366,
6+
viewportHeight: 768,
7+
devServer: {
8+
framework: 'vue',
9+
bundler: 'vite',
10+
},
11+
},
12+
});

cypress/support/commands.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/// <reference types="cypress" />
2+
// ***********************************************
3+
// This example commands.ts shows you how to
4+
// create various custom commands and overwrite
5+
// existing commands.
6+
//
7+
// For more comprehensive examples of custom
8+
// commands please read more here:
9+
// https://on.cypress.io/custom-commands
10+
// ***********************************************
11+
//
12+
//
13+
// -- This is a parent command --
14+
// Cypress.Commands.add('login', (email, password) => { ... })
15+
//
16+
//
17+
// -- This is a child command --
18+
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19+
//
20+
//
21+
// -- This is a dual command --
22+
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23+
//
24+
//
25+
// -- This will overwrite an existing command --
26+
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27+
//
28+
// declare global {
29+
// namespace Cypress {
30+
// interface Chainable {
31+
// login(email: string, password: string): Chainable<void>
32+
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
33+
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
34+
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
35+
// }
36+
// }
37+
// }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
7+
<title>Components App</title>
8+
</head>
9+
<body>
10+
<div data-cy-root></div>
11+
</body>
12+
</html>

cypress/support/component.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// ***********************************************************
2+
// This example support/component.ts is processed and
3+
// loaded automatically before your test files.
4+
//
5+
// This is a great place to put global configuration and
6+
// behavior that modifies Cypress.
7+
//
8+
// You can change the location of this file or turn off
9+
// automatically serving support files with the
10+
// 'supportFile' configuration option.
11+
//
12+
// You can read more here:
13+
// https://on.cypress.io/configuration
14+
// ***********************************************************
15+
16+
// Import commands.js using ES2015 syntax:
17+
import './commands';
18+
19+
// Alternatively you can use CommonJS syntax:
20+
// require('./commands')
21+
22+
import { mount } from 'cypress/vue';
23+
24+
// Augment the Cypress namespace to include type definitions for
25+
// your custom command.
26+
// Alternatively, can be defined in cypress/support/component.d.ts
27+
// with a <reference path="./component" /> at the top of your spec.
28+
declare global {
29+
namespace Cypress {
30+
interface Chainable {
31+
mount: typeof mount;
32+
}
33+
}
34+
}
35+
36+
Cypress.Commands.add('mount', mount);
37+
38+
export function getInt(max: number) {
39+
return Math.floor(Math.random() * max);
40+
}
41+
42+
export function getRandomSequence(maxLength: number) {
43+
const sequence: number[] = [];
44+
45+
let newLength = maxLength;
46+
let prev: number | undefined = undefined;
47+
48+
for (let i = 0; i < newLength; i++) {
49+
const next = getInt(maxLength);
50+
51+
if (typeof prev === 'undefined') {
52+
prev = next;
53+
continue;
54+
}
55+
if (prev === next) {
56+
newLength++;
57+
continue;
58+
}
59+
60+
prev = next;
61+
sequence.push(next);
62+
}
63+
64+
return sequence;
65+
}

demo/useFakeData.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { watch, computed, reactive } from 'vue';
22

3-
export function useFakeData() {
3+
export function useFakeData(length = 10) {
44
const parsedStart = parseInt(sessionStorage.getItem('firstNumber') || '0');
55
const parsedEnd = parseInt(sessionStorage.getItem('lastNumber') || '0');
66
const parsedLength = parsedEnd - parsedStart + 1;
@@ -11,7 +11,7 @@ export function useFakeData() {
1111
const maxText = isMobile ? 100 : 320;
1212

1313
const sections = reactive(
14-
Array.from({ length: parsedLength <= 1 ? 10 : parsedLength }, (_, index) => ({
14+
Array.from({ length: parsedLength <= 1 ? length : parsedLength }, (_, index) => ({
1515
id: `title_${parsedStart + index}`,
1616
title: `${parsedStart + index} `.repeat(6).toUpperCase(),
1717
text: 'Text '.repeat(getInt(minText, maxText)),

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dist/*"
1010
],
1111
"scripts": {
12-
"build": "vue-tsc && vite build",
12+
"build": "vue-tsc && vite build && rimraf dist/_redirects dist/favicon.ico",
1313
"build:app": "vue-tsc && vite build --mode app",
1414
"dev": "vite",
1515
"preview": "vite preview"
@@ -19,7 +19,9 @@
1919
"@types/node": "^18.11.18",
2020
"@vitejs/plugin-vue": "^4.0.0",
2121
"animated-scroll-to": "^2.3.0",
22+
"cypress": "^12.3.0",
2223
"prettier": "^2.8.2",
24+
"rimraf": "^4.1.0",
2325
"typescript": "^4.9.4",
2426
"vite": "^4.0.4",
2527
"vite-plugin-dts": "^1.7.1",

tests/App.cy.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// @ts-ignore
2+
import App from './App.vue';
3+
import { getInt, getRandomSequence } from '../cypress/support/component';
4+
5+
it('Should give priority to URL hash on mount', () => {
6+
const targetsLength = 30;
7+
8+
cy.mount(App, {
9+
props: {
10+
targetsLength,
11+
},
12+
});
13+
14+
const randomIndex = getInt(targetsLength);
15+
16+
cy.get('a')
17+
.eq(randomIndex)
18+
.click()
19+
.should('have.class', 'active')
20+
.invoke('attr', 'href')
21+
.then((href) => {
22+
cy.hash().should('eq', href);
23+
});
24+
});
25+
26+
it.only('Should set active clicked links without scroll interferences', () => {
27+
const targetsLength = 30;
28+
29+
cy.mount(App, {
30+
props: {
31+
targetsLength,
32+
},
33+
});
34+
35+
const randomIndices = getRandomSequence(targetsLength);
36+
37+
randomIndices.forEach((index) => {
38+
cy.wait(100); // Trigger smoothscroll
39+
cy.get('a').eq(index).click().should('have.class', 'active');
40+
cy.wait(100);
41+
});
42+
});
43+
44+
/* it('Should update targets on cancel while scrolling from click', () => {});
45+
46+
it('Should jump to first target', () => {});
47+
48+
it('Should jump to last target', () => {});
49+
50+
it('Should toggle functionalities below minWidth', () => {}); */

tests/App.vue

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue';
3+
import { useFakeData } from '../demo/useFakeData';
4+
import { useActive } from '../src/useActive';
5+
6+
const props = defineProps<{
7+
targetsLength: number;
8+
}>();
9+
10+
const { sections, menuItems } = useFakeData(props.targetsLength);
11+
12+
const targets = computed(() => sections.map(({ id }) => id));
13+
const { setActive, isActive } = useActive(targets);
14+
</script>
15+
16+
<template>
17+
<div class="Wrapper">
18+
<div class="Content">
19+
<section v-for="section in sections" :key="section.id">
20+
<h1 :id="section.id">{{ section.title }}</h1>
21+
<p>{{ section.text }}</p>
22+
</section>
23+
</div>
24+
<nav>
25+
<ul>
26+
<li v-for="item in menuItems" :key="item.href">
27+
<a
28+
@click="setActive(item.href)"
29+
:href="`#${item.href}`"
30+
:class="{ active: isActive(item.href) }"
31+
>{{ item.label }}</a
32+
>
33+
</li>
34+
</ul>
35+
</nav>
36+
</div>
37+
</template>
38+
39+
<style>
40+
html {
41+
scroll-behavior: smooth;
42+
}
43+
</style>
44+
45+
<style scoped>
46+
.Wrapper {
47+
display: grid;
48+
grid-template-columns: 800px 300px;
49+
}
50+
51+
nav {
52+
position: fixed;
53+
right: 0;
54+
top: 0;
55+
}
56+
57+
.active {
58+
background-color: red;
59+
}
60+
</style>

0 commit comments

Comments
 (0)