Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ test-a11y-results/
nala/tools/landing-page-builder/TEST-PLAN.md
nala/tools/landing-page-builder/MANUAL-TEST-PLAN.md
nala/utils/auth.json

# AI / editor local state
.cursor/
CLAUDE.local.md
*.ai.local
107 changes: 107 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is **da-bacom** (Adobe for Business), a Franklin/Helix-based project for business.adobe.com. It uses AEM (Adobe Experience Manager) with the milo-college architecture pattern.

- **Org**: adobecom
- **Main branch**: `main`
- **Stage branch**: `stage`
- **Live**: https://main--da-bacom--adobecom.aem.live
- **Stage**: https://stage--da-bacom--adobecom.aem.live

## Commands

### Development
```bash
aem up # Start local server at http://localhost:3000
```
For Milo integration testing: run `npm run libs` in the Milo folder (port 6456), then add `?milolibs=local` to URLs.

### Unit Tests (Web Test Runner)
```bash
npm run test # Run all unit tests with coverage
npm run test:watch # Watch mode
npm run test:file # Run specific test file
npm run test:file:watch # Run specific test file in watch mode
```

### E2E Tests (Playwright/Nala)
```bash
npm run nala local # All tests locally
npm run nala local accordion.test.js # Specific test file
npm run nala local @accordion # Tests by tag
npm run nala local mode=ui # UI mode
npm run nala stage # Against stage
npm run nala:login # Login to DA (required for LPB tests)
```

Nala options: `browser=chrome|firefox|webkit`, `device=desktop|mobile`, `mode=headless|ui|debug|headed`, `milolibs=local|prod|code|<branch>`

**Landing Page Builder tests** require DA login and use env vars:
```bash
npm run nala:login
LPB_REF=stage npm run nala local @lpb-non-e2e
LPB_URL=http://localhost:3000 npm run nala local landing-page-builder.test.js
```

### Linting
```bash
npm run lint # JS + CSS
npm run lint:fix # Fix JS + CSS
npm run lint:js # JS only
npm run lint:css # CSS only
```

Setup a hook to run lint:fix after each file edit, e.g. Cursor:
```json
// .cursor/hooks.json:
{
"hooks": {
"afterFileEdit": [
"npm run lint:fix"
]
}
}
```

## Architecture

### Block System
`/blocks/` contains reusable UI components following the Franklin block pattern. Each block is a directory with its own JS and CSS files.

### Tools
`/tools/` contains authoring tools:
- **`/generator/`** — Landing Page Builder (LPB), the primary active development area. Key files:
- `landing-page.js` — Main UI and logic
- `form-sections.js` — Form component rendering
- `da-utils.js` — Digital Assets SDK integration
- `paths-config.js` — URL/path construction
- `page-builder-admin.js` — Admin interface
- **`/caas-tag-selector/`**, **`/locale-nav/`**, **`/ui/`** — Other authoring utilities

### Scripts
`/scripts/scripts.js` is the core site initialization script with 100+ locale configurations.

### Testing Structure
- **Unit tests**: `test/**/*.test.js` and `test/**/*.test.html` — run via Web Test Runner
- **E2E tests**: `nala/**/*.test.js` — run via Playwright
- `nala/libs/config.js` — environment configuration
- `nala/libs/constants.js` — shared constants and URLs
- `nala/tools/landing-page-builder/` — LPB-specific E2E tests with their own README

### Logging
Use `window.lana.log()` instead of `console.log()`. Logs go to Splunk.
```js
window.lana.log('message', { severity: 'warning', tags: 'block-name' });
```
Severity levels: `critical`, `error`, `warning`, `info`, `debug`.

## Key Conventions

- **Pre-commit hooks** (Husky) run ESLint, Stylelint, and tests automatically. Bypass with `git commit --no-verify` only when necessary.
- **LPB E2E tests** use stable reusable slugs (`nala-auto-guide-gated-us`, etc.) and a cleanup script: `node nala/tools/landing-page-builder/cleanup-generated-pages.js`
- **Unit test coverage** excludes: `**/mocks/**`, `**/node_modules/**`, `**/test/**`, `**/deps/**`
- **ESLint config**: airbnb-base + react-hooks + compat + ecmalist
1 change: 1 addition & 0 deletions head.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const { hostname, search } = window.location;
if (!['.aem.', '.hlx.', '.stage.', 'local'].some((i) => hostname.includes(i))) return '/libs';
const branch = new URLSearchParams(search).get('milolibs') || 'main';
if (!/^[a-zA-Z0-9_-]+$/.test(branch)) throw new Error('Invalid branch name.');
if (branch === 'local') return 'http://localhost:6456/libs';
if (branch === 'main' && hostname.includes('.stage.')) return '/libs';
return branch.includes('--') ? `https://${branch}.aem.live/libs` : `https://${branch}--milo--adobecom.aem.live/libs`;
Expand Down
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
"description": "Website foundation technology.",
"scripts": {
"test": "npx playwright install && wtr --config ./web-test-runner.config.mjs \"./test/**/*.test.(js|html)\" --node-resolve --port=2000 --coverage --concurrent-browsers 4 --debug",
"test:watch": "npx playwright install && npm test -- --watch",
"test:watch": "npx playwright install && wtr --config ./web-test-runner.config.mjs \"./test/**/*.test.(js|html)\" --node-resolve --port=2001 --coverage --concurrent-browsers 4 --watch",
"test:file": "npx playwright install && wtr --config ./web-test-runner.config.mjs --node-resolve --port=2000 --coverage",
"test:file:watch": "npx playwright install && wtr --config ./web-test-runner.config.mjs --node-resolve --port=2000 --coverage --watch",
"test:file:watch": "npx playwright install && wtr --config ./web-test-runner.config.mjs --node-resolve --port=2001 --coverage --watch",
"lint": "npm run lint:js && npm run lint:css",
"lint:fix": "npm run lint:js:fix && npm run lint:css:fix",
"lint:js": "eslint .",
"lint:js:fix": "eslint --fix .",
"lint:js:nibble": "eslint-nibble .",
"lint:css": "stylelint 'blocks/**/*.css' 'styles/*.css'",
"lint:css": "stylelint 'blocks/**/*.css' 'styles/*.css' 'tools/**/*.css'",
"lint:css:fix": "stylelint --fix 'blocks/**/*.css' 'styles/*.css' 'tools/**/*.css'",
"prepare": "husky install",
"nala": "node nala/utils/nala.run.js",
"nala:login": "node nala/utils/da-login.js",
Expand Down
23 changes: 22 additions & 1 deletion scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ const CONFIG = {
onlybanner: true,
};

const PLAY_SVG = '<svg xmlns="http://www.w3.org/2000/svg" height="18" viewBox="0 0 18 18" width="18" class="icon-milo icon-milo-play"><path fill="currentColor" fill-rule="evenodd" d="M4.73,2H3.5a.5.5,0,0,0-.5.5v13a.5.5,0,0,0,.5.5H4.73a1,1,0,0,0,.5035-.136l11.032-6.433a.5.5,0,0,0,0-.862L5.2335,2.136A1,1,0,0,0,4.73,2Z"/></svg>';

const eagerLoad = (img) => {
img?.setAttribute('loading', 'eager');
img?.setAttribute('fetchpriority', 'high');
Expand Down Expand Up @@ -191,6 +193,7 @@ export function setLibs(location) {
const { hostname, search } = location;
if (!['.aem.', '.hlx.', '.stage.', 'local', '.da.'].some((i) => hostname.includes(i))) return '/libs';
const branch = new URLSearchParams(search).get('milolibs') || 'main';
if (!/^[a-zA-Z0-9_-]+$/.test(branch)) throw new Error('Invalid branch name.');
if (branch === 'local') return 'http://localhost:6456/libs';
if (branch === 'main' && hostname.includes('.stage.')) return '/libs';
return branch.includes('--') ? `https://${branch}.aem.live/libs` : `https://${branch}--milo--adobecom.aem.live/libs`;
Expand Down Expand Up @@ -227,6 +230,23 @@ export function transformExlLinks(locale) {
});
}

export function injectMarqueePlayIcon(MILO_EVENTS) {
const marquee = document.querySelector('.marquee, .hero-marquee');
const marqueePlayIcon = marquee?.querySelector('span.icon-play');
if (!marqueePlayIcon) return;

marqueePlayIcon.innerHTML = PLAY_SVG;
marqueePlayIcon.dataset.svgInjected = 'true';
marqueePlayIcon.classList.add('margin-inline-end');
document.addEventListener(MILO_EVENTS.DEFERRED, async () => {
const marqueePlaySvg = marqueePlayIcon.querySelector('svg');
const { default: loadIcons } = await import(`${LIBS}/features/icons/icons.js`);
delete marqueePlayIcon.dataset.svgInjected;
await loadIcons([marqueePlayIcon]);
marqueePlaySvg?.remove();
}, { once: true });
}

export const EVENT_LIBS = (() => {
const version = 'v1';
const { hostname, search } = window.location;
Expand All @@ -242,7 +262,7 @@ let eventsError;

async function loadPage() {
const {
loadArea, loadLana, setConfig, createTag, getMetadata, getLocale,
loadArea, loadLana, setConfig, createTag, getMetadata, getLocale, MILO_EVENTS,
} = await import(`${LIBS}/utils/utils.js`);

let eventUtils;
Expand Down Expand Up @@ -301,6 +321,7 @@ async function loadPage() {
endpointStage: 'https://business.stage.adobe.com/lana/ll',
});
transformExlLinks(getLocale(CONFIG.locales));
injectMarqueePlayIcon(MILO_EVENTS);

await loadArea();

Expand Down
15 changes: 15 additions & 0 deletions styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ body.disable-scroll #adbMsgClientWrapper #adbmsgContainer .adbmsgCta {
padding: 0;
}

/* hide the first play icon if there is a second play icon */
.marquee span.icon-play svg:has(+ svg),
.hero-marquee span.icon-play svg:has(+ svg) {
display: none;
}

@media screen and (max-width: 375px) {
.brick.iswa {
min-height: 540px;
Expand All @@ -258,6 +264,10 @@ body.disable-scroll #adbMsgClientWrapper #adbmsgContainer .adbmsgCta {
height: 100%;
padding: unset
}

.hero-marquee.iswa div.foreground.cols-2 .copy {
padding-inline-end: 0;
}
}

@media screen and (min-width: 1200px) {
Expand Down Expand Up @@ -288,10 +298,15 @@ body.disable-scroll #adbMsgClientWrapper #adbmsgContainer .adbmsgCta {
letter-spacing: -2px;
line-height: 98%;
}

.iswa h2.heading-s {
font-size: 44px;
}
}

@media screen and (min-width: 1440px) {
main.iswa-main {
--grid-container-width: 1440px;
--grid-margins-width: calc((100% - var(--grid-container-width)) / 2);
}
}
42 changes: 41 additions & 1 deletion test/scripts/scripts.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { readFile, setViewport } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { setLibs, LIBS, getLCPImages, transformExlLinks, applyIswaTypography } from '../../scripts/scripts.js';
import {
setLibs,
LIBS,
getLCPImages,
transformExlLinks,
applyIswaTypography,
injectMarqueePlayIcon,
} from '../../scripts/scripts.js';

describe('Libs', () => {
const tests = [
Expand Down Expand Up @@ -202,3 +209,36 @@ describe('ISWA Typography', () => {
expect(document.querySelector('.iswa-main')).to.not.exist;
});
});

describe('injectMarqueePlayIcon', () => {
const MILO_EVENTS = { DEFERRED: 'milo:deferred-test' };

beforeEach(() => {
document.body.innerHTML = '';
});

it('injects play SVG and marks the icon when marquee contains span.icon-play', () => {
document.body.innerHTML = `
<div class="marquee">
<span class="icon icon-play"></span>
</div>
`;
injectMarqueePlayIcon(MILO_EVENTS);
const icon = document.querySelector('span.icon-play');
expect(icon.querySelector('svg.icon-milo-play')).to.exist;
expect(icon.dataset.svgInjected).to.equal('true');
expect(icon.classList.contains('margin-inline-end')).to.be.true;
});

it('does nothing when there is no marquee', () => {
document.body.innerHTML = '<div class="aside">Aside block<span class="icon icon-play"></span></div>';
injectMarqueePlayIcon(MILO_EVENTS);
expect(document.querySelector('svg')).to.not.exist;
});

it('does nothing when marquee has no play icon', () => {
document.body.innerHTML = '<div class="marquee"><span class="icon icon-other"></span></div>';
injectMarqueePlayIcon(MILO_EVENTS);
expect(document.querySelector('svg')).to.not.exist;
});
});
Loading