Skip to content

Commit dd7d202

Browse files
Merge pull request #26 from HubSpotWebTeam/feature/add-cypress-configuration
Feature/add cypress configuration
2 parents 5233422 + 54fc821 commit dd7d202

12 files changed

Lines changed: 1371 additions & 138 deletions

File tree

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
node: [22, 24]
14+
node: [24]
1515

1616
name: Node ${{ matrix.node }} lint
1717
steps:
18-
- uses: actions/checkout@v5
18+
- uses: actions/checkout@v6
1919
- name: Run unit tests
2020
uses: actions/setup-node@v6
2121
with:

.github/workflows/pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
node: [22, 24]
14+
node: [24]
1515

1616
name: Node ${{ matrix.node }} sample
1717
steps:
18-
- uses: actions/checkout@v5
18+
- uses: actions/checkout@v6
1919
- name: Run unit tests
2020
uses: actions/setup-node@v6
2121
with:

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v22
1+
v24

.stylelintrc.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"extends": "stylelint-config-standard-scss",
3+
"rules": {
4+
"length-zero-no-unit": null,
5+
"declaration-empty-line-before": null,
6+
"no-empty-source": null,
7+
"at-rule-no-unknown": [
8+
true,
9+
{
10+
"ignoreAtRules": ["function", "if", "else", "for", "each", "include", "mixin", "extend"]
11+
}
12+
],
13+
"scss/dollar-variable-pattern": null,
14+
"scss/at-import-no-partial-leading-underscore": null,
15+
"selector-class-pattern": null,
16+
"declaration-block-no-redundant-longhand-properties": null,
17+
"scss/at-import-partial-extension": null,
18+
"alpha-value-notation": null,
19+
"scss/percent-placeholder-pattern": null,
20+
"scss/at-mixin-argumentless-call-parentheses": null,
21+
"color-function-notation": null,
22+
"scss/dollar-variable-empty-line-before": null,
23+
"shorthand-property-no-redundant-values": null,
24+
"no-duplicate-selectors": null,
25+
"scss/double-slash-comment-whitespace-inside": null,
26+
"font-family-name-quotes": null,
27+
"media-feature-range-notation": [
28+
"context",
29+
{
30+
"severity": "warning"
31+
}
32+
],
33+
"value-keyword-case": [
34+
"lower",
35+
{
36+
"camelCaseSvgKeywords": true
37+
}
38+
]
39+
}
40+
}

CLAUDE.md

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Overview
66

7-
This is an ESLint configuration package (`@hs-web-team/eslint-config-node`) for HubSpot Marketing WebTeam projects. It provides shared ESLint rules and Prettier configuration for both backend Node.js projects and browser/React applications.
7+
This is a configuration package (`@hs-web-team/eslint-config-node`) for HubSpot Marketing WebTeam projects. It provides shared configurations for:
8+
- **ESLint** rules for Node.js and browser/React projects
9+
- **Prettier** formatting configuration
10+
- **Stylelint** rules for SCSS/CSS linting
11+
- **Cypress** E2E testing configuration
812

913
## Key Commands
1014

@@ -66,27 +70,76 @@ When testing changes to this package in downstream projects, you'll typically:
6670
- Arrow parens: avoid, LF line endings, single attribute per line
6771
- Override for YAML files: uses double quotes instead of single
6872

73+
### Stylelint Configuration
74+
- **`.stylelintrc.json`**: Shared Stylelint configuration for SCSS/CSS linting
75+
- Extends `stylelint-config-standard-scss` for standard SCSS linting rules
76+
- Customized rules for HubSpot projects:
77+
- Disables strict class name patterns and variable naming patterns
78+
- Allows zero units (e.g., `0px`), redundant longhand properties, and duplicate selectors
79+
- Configures SCSS-specific rules to ignore common directives (`@function`, `@if`, `@mixin`, etc.)
80+
- Enforces lowercase keywords with camelCase for SVG keywords
81+
- Sets media feature range notation to "context" with warning severity
82+
- Consuming projects extend this via `.stylelintrc.json`: `{ "extends": "@hs-web-team/eslint-config-node/.stylelintrc.json" }`
83+
- Documentation: `examples/stylelint-usage.md`
84+
85+
### Cypress Configuration
86+
- **`cypress.config.cjs`**: Shared Cypress configuration for E2E testing
87+
- Uses CommonJS (`require`) syntax
88+
- Core settings:
89+
- Disables Chrome web security for cross-origin testing
90+
- Timeouts: 20s default command, page load, and response timeouts
91+
- Viewport: 1920x1080
92+
- Retries: 1 retry in run mode, 0 in open mode
93+
- Screenshots on failure enabled, video disabled by default
94+
- Uses **esbuild** preprocessor for fast bundling (much faster than webpack)
95+
- Includes Cucumber preprocessor setup with native esbuild support
96+
- Native TypeScript support via esbuild (no additional loader needed)
97+
- `@badeball/cypress-cucumber-preprocessor`, `@bahmutov/cypress-esbuild-preprocessor`, `esbuild`, and `js-yaml` are all bundled — consumers only need to install `cypress`
98+
- Spec pattern: `cypress/e2e/*.cy.js`
99+
- Exported utilities:
100+
- `config`: Main Cypress configuration object
101+
- `envs`: Environment constants (DEV, QA, PROD)
102+
- `getDevBaseUrl()`: Reads baseUrl from `hubspot.config.yml` DEV portal
103+
- `getBaseUrls()`: Reads URLs from `.ci/config.yml` for multiple environments
104+
- HubSpot-specific: Designed to work with HubSpot CMS projects using `hubspot.config.yml`
105+
- Documentation: `examples/cypress-usage.md`
106+
69107
### Package Details
70108
- **Type**: ESM module (`"type": "module"`)
71-
- **Node requirement**: >= 22
109+
- **Node requirement**: >= 24
72110
- **Exports**:
73-
- Main export (`.`): `index.js` - Node.js configuration
74-
- Browser export (`./browser`): `browser.js` - Browser/React configuration
111+
- Main export (`.`): `index.js` - Node.js ESLint configuration
112+
- Browser export (`./browser`): `browser.js` - Browser/React ESLint configuration
113+
- Prettier export (`./.prettierrc.json`): `.prettierrc.json` - Prettier configuration
114+
- Stylelint export (`./.stylelintrc.json`): `.stylelintrc.json` - Stylelint configuration
115+
- Cypress export (`./cypress.config`): `cypress.config.cjs` (runtime), `cypress.config.d.ts` (types)
75116
- **Binary command**: `add-prettier` maps to `bin/add-prettier-scripts.js`
76117

77118
### Migration Context
78119
This package is currently on v3, which uses ESLint 9's flat config format. The project has migrated from:
79120
- v1 → v2: See `docs/MIGRATION-V2.md`
80-
- v2 → v3: See `docs/MIGRATION-V3.md` (ESLint 9 flat config migration requiring Node.js 22+)
121+
- v2 → v3: See `docs/MIGRATION-V3.md` (ESLint 9 flat config migration requiring Node.js 24+)
81122

82123
## Important Notes
83124

84-
- This package supports both **backend Node.js projects** (default export) and **browser/React projects** (`/browser` export)
85-
- The configuration uses ESLint 9's flat config format (not the legacy `.eslintrc` format)
86-
- Downstream projects extend this config in their `eslint.config.js` by spreading the imported config:
87-
- Node.js: `...wtConfig`
88-
- Browser: `...wtBrowserConfig`
89-
- Mixed module systems: `bin/add-prettier-scripts.js` uses CommonJS (`require`) while the main package is ESM
90-
- CI runs on Node 22 and 24 (see `.github/workflows/pr.yml`)
125+
- This package supports multiple use cases:
126+
- **ESLint**: Backend Node.js projects (default export) and browser/React projects (`/browser` export)
127+
- **Prettier**: Code formatting (`./.prettierrc.json` export)
128+
- **Stylelint**: SCSS/CSS linting (`./.stylelintrc.json` export)
129+
- **Cypress**: E2E testing (`./cypress.config` export)
130+
- ESLint configuration uses ESLint 9's flat config format (not the legacy `.eslintrc` format)
131+
- Downstream projects extend configs:
132+
- ESLint (Node.js): `...wtConfig` in `eslint.config.js`
133+
- ESLint (Browser): `...wtBrowserConfig` in `eslint.config.js`
134+
- Prettier: Import via `.prettierrc.js` using `require('@hs-web-team/eslint-config-node/.prettierrc.json')`
135+
- Stylelint: Extend in `.stylelintrc.json` with `"extends": "@hs-web-team/eslint-config-node/.stylelintrc.json"`
136+
- Cypress: Import and spread in `cypress.config.js`
137+
- Mixed module systems:
138+
- Main package is ESM (`index.js`, `browser.js`)
139+
- Utility scripts use CommonJS (`bin/add-prettier-scripts.js`, `cypress.config.cjs`)
140+
- CI runs on Node 24 (see `.github/workflows/pr.yml`)
91141
- No automated tests currently (`npm test` will fail with "Error: no test specified")
92-
- Detailed browser configuration documentation available in `examples/browser-usage.md`
142+
- Detailed documentation available in `examples/`:
143+
- `browser-usage.md` - Browser/React ESLint configuration
144+
- `stylelint-usage.md` - Stylelint configuration
145+
- `cypress-usage.md` - Cypress E2E testing configuration

README.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This package provides ESLint rules and configurations for **Hubspot Marketing We
88

99
- [Node.js Setup](#nodejs-setup)
1010
- [Browser/React Setup](#browserreact-setup)
11+
- [Stylelint Setup](#stylelint-setup)
12+
- [Cypress Setup](#cypress-setup)
1113
- [Where to use it](#where-to-use-it)
1214
- [Using the Prettier Scripts](#using-the-prettier-scripts)
1315
<!-- index-end -->
@@ -78,14 +80,60 @@ This package provides ESLint rules and configurations for **Hubspot Marketing We
7880

7981
For detailed browser configuration documentation and migration guides, see [examples/browser-usage.md](./examples/browser-usage.md).
8082

83+
## Stylelint Setup
84+
85+
This package provides shared Stylelint configuration for SCSS/CSS linting.
86+
87+
1. Install dependencies
88+
89+
```sh
90+
npm i -D stylelint stylelint-config-standard-scss
91+
```
92+
93+
2. Create `.stylelintrc.json` in project root
94+
95+
```json
96+
{
97+
"extends": "@hs-web-team/eslint-config-node/.stylelintrc.json"
98+
}
99+
```
100+
101+
3. Add scripts to package.json
102+
103+
```json
104+
{
105+
"scripts": {
106+
"stylelint:check": "stylelint '**/*.{css,scss}'",
107+
"stylelint:fix": "stylelint '**/*.{css,scss}' --fix"
108+
}
109+
}
110+
```
111+
112+
For detailed Stylelint configuration documentation, see [examples/stylelint-usage.md](./examples/stylelint-usage.md).
113+
114+
## Cypress Setup
115+
116+
This package provides shared Cypress configuration for E2E testing.
117+
118+
1. Install Cypress
119+
120+
```sh
121+
npm i -D cypress@15
122+
```
123+
124+
For detailed Cypress configuration and migration documentation, see [examples/cypress-usage.md](./examples/cypress-usage.md).
125+
81126
## Where to use it
82127

83-
This package provides two configurations:
128+
This package provides multiple configurations:
84129

85130
- **Node.js configuration** (default export): For backend Node.js projects
86131
- **Browser configuration** (`/browser` export): For browser-based projects including React applications
132+
- **Stylelint configuration** (`.stylelintrc.json` export): For SCSS/CSS linting
133+
- **Cypress configuration** (`cypress.config` export): For E2E testing with Cypress
134+
- **Prettier configuration** (`.prettierrc.json` export): For code formatting
87135

88-
Choose the appropriate configuration based on your project type.
136+
Choose the appropriate configurations based on your project needs.
89137

90138
## Using the Prettier Scripts
91139

@@ -115,4 +163,4 @@ See [MIGRATION-V2.md](./docs/MIGRATION-V2.md)
115163

116164
## Migration from v2 to v3
117165

118-
See [MIGRATION-V3.md](./docs/MIGRATION-V3.md)
166+
See [MIGRATION-V3.md](./docs/MIGRATION-V3.md)

cypress.config.cjs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
const yaml = require('js-yaml');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const createBundler = require('@bahmutov/cypress-esbuild-preprocessor');
5+
const { addCucumberPreprocessorPlugin } = require('@badeball/cypress-cucumber-preprocessor');
6+
const { createEsbuildPlugin } = require('@badeball/cypress-cucumber-preprocessor/esbuild');
7+
8+
const DEV = 'DEV';
9+
const QA = 'qa';
10+
const PROD = 'prod';
11+
const currentEnv = QA;
12+
13+
const envs = {
14+
currentEnv,
15+
DEV,
16+
QA,
17+
PROD,
18+
};
19+
20+
/**
21+
* @description Recursively climbs up the filepath, until it finds what directory
22+
* the directory where the hubspot.config.yml file is located.
23+
*
24+
* @param {string} currDir - the current working directory path to search from
25+
* @returns {string} The absolute path of the project's root directory
26+
*/
27+
const getRootDir = currDir => {
28+
if (fs.existsSync(path.join(currDir, 'hubspot.config.yml'))) return currDir;
29+
const parentDir = path.dirname(currDir);
30+
if (parentDir === currDir) {
31+
throw new Error('Error: Could not find the hubspot.config.yml file within the projects directories.');
32+
}
33+
return getRootDir(parentDir);
34+
};
35+
36+
/**
37+
* @returns {string|null} The `baseUrl` set for the `DEV` portal in `hubspot.config.yml`
38+
* or `null` if this is not the dev environment or no such property exists.
39+
*/
40+
const getDevBaseUrl = () => {
41+
try {
42+
global.console.log(
43+
'To test a dev URL, add the `baseUrl` property to your `DEV` portal configuration in `hubspot.config.yml`',
44+
);
45+
46+
const root = getRootDir(__dirname);
47+
const configPath = path.resolve(root, 'hubspot.config.yml');
48+
const config = fs.readFileSync(configPath, 'utf8');
49+
const { portals } = yaml.load(config);
50+
const devPortal = portals.find(portal => portal.name === 'DEV');
51+
const devBaseUrl = devPortal.baseUrl;
52+
return devBaseUrl || null;
53+
} catch (error) {
54+
global.console.error(error);
55+
return null;
56+
}
57+
};
58+
59+
/**
60+
* @description Get the baseUrls for different environments from the ci config file for local test execution.
61+
* @returns {object} baseUrls - The base urls object
62+
*/
63+
const getBaseUrls = () => {
64+
let fileContents = '';
65+
let ciConfig = {};
66+
const baseUrls = {};
67+
baseUrls[envs.DEV] = getDevBaseUrl();
68+
69+
const fileExist = fs.existsSync('.ci/config.yml');
70+
if (fileExist) {
71+
fileContents = fs.readFileSync('.ci/config.yml', 'utf8');
72+
ciConfig = yaml.load(fileContents);
73+
if (ciConfig.regression.e2eTestEnvironment && ciConfig.regression.e2eTestEnvironment.length > 0) {
74+
try {
75+
ciConfig.regression.e2eTestEnvironment.forEach(item => {
76+
const envName = item.name;
77+
baseUrls[envName] = item.url;
78+
});
79+
} catch (error) {
80+
console.error('Error reading the base urls from the ci config file:', error);
81+
return null;
82+
}
83+
}
84+
}
85+
return baseUrls || null;
86+
};
87+
88+
async function setupNodeEvents(on, config) {
89+
// This is required for the preprocessor to be able to generate JSON reports after each run
90+
await addCucumberPreprocessorPlugin(on, config);
91+
92+
// Use esbuild for fast TypeScript and feature file processing
93+
on(
94+
'file:preprocessor',
95+
createBundler({ plugins: [createEsbuildPlugin(config)] }),
96+
);
97+
98+
return config;
99+
}
100+
101+
const e2e = {
102+
specPattern: 'cypress/e2e/*.cy.js',
103+
setupNodeEvents,
104+
};
105+
106+
const config = {
107+
chromeWebSecurity: false,
108+
defaultCommandTimeout: 20000,
109+
e2e,
110+
numTestsKeptInMemory: 0,
111+
pageLoadTimeout: 20000,
112+
port: 3500,
113+
responseTimeout: 20000,
114+
retries: {
115+
runMode: 1,
116+
openMode: 0,
117+
},
118+
screenshotOnRunFailure: true,
119+
trashAssetsBeforeRuns: true,
120+
video: false,
121+
viewportHeight: 1080,
122+
viewportWidth: 1920,
123+
};
124+
125+
module.exports = {
126+
config,
127+
envs,
128+
getDevBaseUrl,
129+
getBaseUrls,
130+
setupNodeEvents,
131+
};

0 commit comments

Comments
 (0)