Align tests and docs with Pitch, the customer Home, and the terminal dock.
Page, crawl, Playwright and Lighthouse coverage now include both pitch locales, and the contributor docs match the new route table and chrome. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@ Contributor and agent guide for this repository. Read this before changing routi
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This is a bilingual recruiter and B2B portfolio for a software/DevOps engineer. German is the default language and is served at `/`. English is the full second version and is served under `/en`. Every public page exists in both locales.
|
This is a bilingual portfolio for a software/DevOps engineer. German is the default language and is served at `/`. English is the full second version and is served under `/en`. Home is the direct-customer experience. `/pitch` is the recruiter-oriented page and is reachable without sitting in the primary navigation. Every public page exists in both locales.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -93,6 +93,7 @@ German and English are written idiomatically per language, never machine-transla
|
|||||||
## i18n rules
|
## i18n rules
|
||||||
|
|
||||||
- German at `/`, English under `/en`
|
- German at `/`, English under `/en`
|
||||||
|
- Primary navigation is Home, Services (with four children), Projects, About and Contact. Stack and Pitch stay reachable from content and the terminal, not from the primary nav.
|
||||||
- Every route id exists in both locales
|
- Every route id exists in both locales
|
||||||
- New pages are added by extending the route-id table in `src/app/core/routing`, never by hard-coding paths
|
- New pages are added by extending the route-id table in `src/app/core/routing`, never by hard-coding paths
|
||||||
- Do not swap `LOCALE_ID`; the active locale is `AppLocale` from `LocaleService`
|
- Do not swap `LOCALE_ID`; the active locale is `AppLocale` from `LocaleService`
|
||||||
@@ -154,7 +155,7 @@ A change is not done until:
|
|||||||
| ----------- | -------------------------------------------------------------------------------- |
|
| ----------- | -------------------------------------------------------------------------------- |
|
||||||
| Foundation | Tooling, tokens, shell, routing and content contracts, SSR safety, test baseline |
|
| Foundation | Tooling, tokens, shell, routing and content contracts, SSR safety, test baseline |
|
||||||
| Content | Bilingual copy data and page composition |
|
| Content | Bilingual copy data and page composition |
|
||||||
| Signature | Systems Map, terminal, dot background and motion |
|
| Signature | Systems Map, terminal dock, dot background and motion |
|
||||||
| Integration | SEO and cross-cutting a11y/performance hardening |
|
| Integration | SEO and cross-cutting a11y/performance hardening |
|
||||||
|
|
||||||
Each branch extends the shared contracts instead of duplicating paths or copy. The Content branch replaces `placeholder-content.ts` and keeps `SITE_CONTENT`. The Signature branch may replace the dot-background internals but must keep the SSR-safe init/teardown contract.
|
Each branch extends the shared contracts instead of duplicating paths or copy. The Content branch replaces `placeholder-content.ts` and keeps `SITE_CONTENT`. The Signature branch may replace the dot-background internals but must keep the SSR-safe init/teardown contract.
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Portfolio
|
# Portfolio
|
||||||
|
|
||||||
Bilingual recruiter and B2B portfolio for Antonio Ledebuhr, a software and DevOps engineer. German is the default language at `/`. English is the full second version under `/en`.
|
Bilingual portfolio for Antonio Ledebuhr, a software and DevOps engineer. German is the default language at `/`. English is the full second version under `/en`. Home speaks to small-company customers. `/pitch` is the recruiter-oriented page and is not in the primary navigation.
|
||||||
|
|
||||||
This repository is the Angular 21 standalone, zoneless, SSR application that serves both locales from one build.
|
This repository is the Angular 21 standalone, zoneless, SSR application that serves both locales from one build.
|
||||||
|
|
||||||
@@ -17,23 +17,24 @@ npm install
|
|||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
| Script | Description |
|
| Script | Description |
|
||||||
| ----------------------------- | ------------------------------------------------------------------------- |
|
| ----------------------------- | -------------------------------------------------------------------------- |
|
||||||
| `npm start` | Start the Angular dev server. |
|
| `npm start` | Start the Angular dev server. |
|
||||||
| `npm run build` | Production build (default configuration), including SSR and prerendering. |
|
| `npm run build` | Production build (default configuration), including SSR and prerendering. |
|
||||||
| `npm run watch` | Development rebuild on change. |
|
| `npm run watch` | Development rebuild on change. |
|
||||||
| `npm test` | Unit tests in watch mode. |
|
| `npm test` | Unit tests in watch mode. |
|
||||||
| `npm run test:ci` | Unit tests once; exits when finished. |
|
| `npm run test:ci` | Unit tests once; exits when finished. |
|
||||||
| `npm run lint` | Run ESLint. |
|
| `npm run lint` | Run ESLint. |
|
||||||
| `npm run lint:fix` | Run ESLint with autofix. |
|
| `npm run lint:fix` | Run ESLint with autofix. |
|
||||||
| `npm run format` | Format the workspace with Prettier. |
|
| `npm run format` | Format the workspace with Prettier. |
|
||||||
| `npm run format:check` | Check formatting without writing. |
|
| `npm run format:check` | Check formatting without writing. |
|
||||||
| `npm run serve:ssr` | Serve the production SSR bundle from `dist/`. |
|
| `npm run serve:ssr` | Serve the production SSR bundle from `dist/`. |
|
||||||
| `npm run serve:ssr:Portfolio` | Alias of `serve:ssr`. |
|
| `npm run serve:ssr:Portfolio` | Alias of `serve:ssr`. |
|
||||||
| `npm run e2e` | Playwright + axe-core against the production SSR bundle. |
|
| `npm run e2e` | Playwright + axe-core against the production SSR bundle. |
|
||||||
| `npm run e2e:install` | Install the pinned Chromium build for Playwright. |
|
| `npm run e2e:install` | Install the pinned Chromium build for Playwright. |
|
||||||
| `npm run lighthouse` | Production build, then Lighthouse CI (`lighthouserc.json`). |
|
| `npm run lighthouse` | Production build, then Lighthouse CI (`lighthouserc.json`). |
|
||||||
| `npm run ci` | lint, format check, tests, then production build. |
|
| `npm run ci` | lint, format check, tests, then production build. |
|
||||||
|
| `npm run cv:link` | Add the visible `/pitch` URL and link annotation to `CV.pdf` (idempotent). |
|
||||||
|
|
||||||
## Local SSR build
|
## Local SSR build
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ npm run build
|
|||||||
npm run serve:ssr
|
npm run serve:ssr
|
||||||
```
|
```
|
||||||
|
|
||||||
The server listens on `http://localhost:4000` unless `PORT` is set. The CV is copied into the browser output at `/cv/CV.pdf`.
|
The server listens on `http://localhost:4000` unless `PORT` is set. The CV is copied into the browser output at `/cv/CV.pdf`. The header no longer offers that download; Pitch and the terminal `navigate cv` command still do. The lower-right terminal dock replaces the old centered command palette (`Ctrl+K` / `⌘K`).
|
||||||
|
|
||||||
`npm run e2e` builds the SSR bundle and serves it on port 4173. `npm run lighthouse` builds, then starts the SSR server on port 4000. Browser binaries and generated reports (`test-results`, `playwright-report`, `.lighthouseci`) are not committed.
|
`npm run e2e` builds the SSR bundle and serves it on port 4173. `npm run lighthouse` builds, then starts the SSR server on port 4000. Browser binaries and generated reports (`test-results`, `playwright-report`, `.lighthouseci`) are not committed.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { pagePath } from './helpers';
|
|||||||
const AXE_PATHS = [
|
const AXE_PATHS = [
|
||||||
pagePath('home', 'de'),
|
pagePath('home', 'de'),
|
||||||
pagePath('home', 'en'),
|
pagePath('home', 'en'),
|
||||||
|
pagePath('pitch', 'de'),
|
||||||
|
pagePath('pitch', 'en'),
|
||||||
pagePath('projects', 'de'),
|
pagePath('projects', 'de'),
|
||||||
pagePath('projects', 'en'),
|
pagePath('projects', 'en'),
|
||||||
pagePath('servicesAi', 'de'),
|
pagePath('servicesAi', 'de'),
|
||||||
@@ -23,8 +25,11 @@ test.describe('accessibility', () => {
|
|||||||
await page.goto(pagePath('home', 'de'));
|
await page.goto(pagePath('home', 'de'));
|
||||||
const pending = page.locator('.reveal-pending');
|
const pending = page.locator('.reveal-pending');
|
||||||
await expect(pending).toHaveCount(0);
|
await expect(pending).toHaveCount(0);
|
||||||
await expect(page.locator('app-systems-map')).toBeVisible();
|
|
||||||
await expect(page.locator('app-case-card').first()).toBeVisible();
|
await expect(page.locator('app-case-card').first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto(pagePath('services', 'de'));
|
||||||
|
await expect(page.locator('.reveal-pending')).toHaveCount(0);
|
||||||
|
await expect(page.locator('app-systems-map')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const path of AXE_PATHS) {
|
for (const path of AXE_PATHS) {
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ test.describe('contact briefing', () => {
|
|||||||
|
|
||||||
await page.locator('#contact-name').fill('Ada');
|
await page.locator('#contact-name').fill('Ada');
|
||||||
await page.locator('#contact-email').fill('ada@example.com');
|
await page.locator('#contact-email').fill('ada@example.com');
|
||||||
await page.locator('#contact-projectType').selectOption('software');
|
|
||||||
await page.locator('#contact-situation').fill('Need a migration.');
|
await page.locator('#contact-situation').fill('Need a migration.');
|
||||||
|
|
||||||
|
const optionalHref = await page.locator('a.submit').getAttribute('href');
|
||||||
|
expect(optionalHref).toMatch(/^mailto:/);
|
||||||
|
|
||||||
|
await page.locator('#contact-projectType').selectOption('unsure');
|
||||||
|
|
||||||
const href = await page.locator('a.submit').getAttribute('href');
|
const href = await page.locator('a.submit').getAttribute('href');
|
||||||
expect(href).toMatch(/^mailto:/);
|
expect(href).toMatch(/^mailto:/);
|
||||||
expect(href).toContain(encodeURIComponent(SITE_CONTENT_DATA.de.contact.mailSubject));
|
expect(href).toContain(encodeURIComponent(SITE_CONTENT_DATA.de.contact.mailSubject));
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { expectNoOverflow, pagePath } from './helpers';
|
|||||||
const CHECKED = [
|
const CHECKED = [
|
||||||
pagePath('home', 'de'),
|
pagePath('home', 'de'),
|
||||||
pagePath('home', 'en'),
|
pagePath('home', 'en'),
|
||||||
|
pagePath('pitch', 'de'),
|
||||||
|
pagePath('pitch', 'en'),
|
||||||
pagePath('projects', 'de'),
|
pagePath('projects', 'de'),
|
||||||
pagePath('projects', 'en'),
|
pagePath('projects', 'en'),
|
||||||
pagePath('servicesAi', 'de'),
|
pagePath('servicesAi', 'de'),
|
||||||
|
|||||||
@@ -38,6 +38,25 @@ test.describe('server-rendered metadata', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pitch has full head metadata and stays out of the primary nav', async ({
|
||||||
|
request,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await expectHead(request, 'pitch', 'de');
|
||||||
|
await expectHead(request, 'pitch', 'en');
|
||||||
|
|
||||||
|
const sitemap = await request.get('/sitemap.xml');
|
||||||
|
const xml = await sitemap.text();
|
||||||
|
expect(xml).toContain('https://antoniolede.de/pitch');
|
||||||
|
expect(xml).toContain('https://antoniolede.de/en/pitch');
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
await ensurePrimaryNavOpen(page);
|
||||||
|
const nav = page.locator('.primary-nav');
|
||||||
|
await expect(nav.locator(`a[href="${pagePath('pitch', 'de')}"]`)).toHaveCount(0);
|
||||||
|
await expect(nav.locator(`a[href="${pagePath('home', 'de')}"]`)).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('unmatched URLs return 404 and a real route stays 200', async ({ request }) => {
|
test('unmatched URLs return 404 and a real route stays 200', async ({ request }) => {
|
||||||
const home = await request.get('/');
|
const home = await request.get('/');
|
||||||
expect(home.status(), 'GET /').toBe(200);
|
expect(home.status(), 'GET /').toBe(200);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { pagePath } from './helpers';
|
|||||||
|
|
||||||
test.describe('Systems Map', () => {
|
test.describe('Systems Map', () => {
|
||||||
test('keeps the card list visible and shows the SVG from 1024px', async ({ page }) => {
|
test('keeps the card list visible and shows the SVG from 1024px', async ({ page }) => {
|
||||||
await page.goto(pagePath('home', 'de'));
|
await page.goto(pagePath('services', 'de'));
|
||||||
|
|
||||||
for (const width of [390, 768, 1024, 1440]) {
|
for (const width of [390, 768, 1024, 1440]) {
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
@@ -21,7 +21,7 @@ test.describe('Systems Map', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('keeps SVG labels inside their shapes at 1024 and 1440', async ({ page }) => {
|
test('keeps SVG labels inside their shapes at 1024 and 1440', async ({ page }) => {
|
||||||
await page.goto(pagePath('home', 'de'));
|
await page.goto(pagePath('services', 'de'));
|
||||||
|
|
||||||
for (const width of [1024, 1440]) {
|
for (const width of [1024, 1440]) {
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
@@ -51,7 +51,7 @@ test.describe('Systems Map', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('every SVG node points at a real route', async ({ page, request }) => {
|
test('every SVG node points at a real route', async ({ page, request }) => {
|
||||||
await page.goto(pagePath('home', 'de'));
|
await page.goto(pagePath('services', 'de'));
|
||||||
await page.setViewportSize({ width: 1440, height: 900 });
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
const hrefs = await page
|
const hrefs = await page
|
||||||
.locator('.systems-map-node')
|
.locator('.systems-map-node')
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
"url": [
|
"url": [
|
||||||
"http://127.0.0.1:4000/",
|
"http://127.0.0.1:4000/",
|
||||||
"http://127.0.0.1:4000/en",
|
"http://127.0.0.1:4000/en",
|
||||||
|
"http://127.0.0.1:4000/pitch",
|
||||||
|
"http://127.0.0.1:4000/en/pitch",
|
||||||
"http://127.0.0.1:4000/leistungen/ai-integration",
|
"http://127.0.0.1:4000/leistungen/ai-integration",
|
||||||
"http://127.0.0.1:4000/en/projects"
|
"http://127.0.0.1:4000/en/projects"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { RouterTestingHarness } from '@angular/router/testing';
|
|||||||
import { routes } from '../app.routes';
|
import { routes } from '../app.routes';
|
||||||
import { SITE_CONTENT } from '../core/content/content.token';
|
import { SITE_CONTENT } from '../core/content/content.token';
|
||||||
import { SITE_CONTENT_DATA } from '../core/content/site-content';
|
import { SITE_CONTENT_DATA } from '../core/content/site-content';
|
||||||
import { SITE_CONFIG } from '../core/content/site-config';
|
|
||||||
import { CASE_STUDY_IDS } from '../core/content/content.contracts';
|
import { CASE_STUDY_IDS } from '../core/content/content.contracts';
|
||||||
import { APP_LOCALES } from '../core/i18n/locale';
|
import { APP_LOCALES } from '../core/i18n/locale';
|
||||||
import { routePath } from '../core/routing/route-paths';
|
import { routePath } from '../core/routing/route-paths';
|
||||||
@@ -67,6 +66,8 @@ describe('page rendering, semantics and accessibility', () => {
|
|||||||
const paths = [
|
const paths = [
|
||||||
'/',
|
'/',
|
||||||
'/en',
|
'/en',
|
||||||
|
'/pitch',
|
||||||
|
'/en/pitch',
|
||||||
'/leistungen/software',
|
'/leistungen/software',
|
||||||
'/en/services/software',
|
'/en/services/software',
|
||||||
'/projekte',
|
'/projekte',
|
||||||
@@ -111,7 +112,7 @@ describe('page rendering, semantics and accessibility', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exposes case anchors, audience entries and the CV download', async () => {
|
it('exposes case anchors and the Rösterei proof on Home', async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }],
|
providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }],
|
||||||
});
|
});
|
||||||
@@ -129,37 +130,48 @@ describe('page rendering, semantics and accessibility', () => {
|
|||||||
for (const path of ['/', '/en']) {
|
for (const path of ['/', '/en']) {
|
||||||
await harness.navigateByUrl(path);
|
await harness.navigateByUrl(path);
|
||||||
const root = harness.routeNativeElement as HTMLElement;
|
const root = harness.routeNativeElement as HTMLElement;
|
||||||
expect(root.querySelector('#recruiters')).toBeTruthy();
|
expect(root.querySelector('#home-proof')).toBeTruthy();
|
||||||
expect(root.querySelector('#companies')).toBeTruthy();
|
expect(root.querySelector('app-case-card')).toBeTruthy();
|
||||||
expect(root.querySelector(`a[href="${SITE_CONFIG.cvAssetPath}"]`)).toBeTruthy();
|
expect(root.querySelector('#recruiters')).toBeNull();
|
||||||
|
expect(root.querySelector('#companies')).toBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(SITE_CONTENT_DATA.de.pages.notFound.hero.headline).toBeTruthy();
|
expect(SITE_CONTENT_DATA.de.pages.notFound.hero.headline).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('places the Systems Map between the profile block and the featured cases', async () => {
|
it('places the Systems Map on the services overview', async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }],
|
providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const harness = await RouterTestingHarness.create();
|
const harness = await RouterTestingHarness.create();
|
||||||
|
|
||||||
|
for (const path of ['/leistungen', '/en/services']) {
|
||||||
|
await harness.navigateByUrl(path);
|
||||||
|
const root = harness.routeNativeElement as HTMLElement;
|
||||||
|
const map = root.querySelector('app-systems-map');
|
||||||
|
expect(map).toBeTruthy();
|
||||||
|
assertPageSemantics(root);
|
||||||
|
}
|
||||||
|
|
||||||
for (const path of ['/', '/en']) {
|
for (const path of ['/', '/en']) {
|
||||||
await harness.navigateByUrl(path);
|
await harness.navigateByUrl(path);
|
||||||
const root = harness.routeNativeElement as HTMLElement;
|
const root = harness.routeNativeElement as HTMLElement;
|
||||||
const metrics = root.querySelector('app-metric-list');
|
expect(root.querySelector('app-systems-map')).toBeNull();
|
||||||
const map = root.querySelector('app-systems-map');
|
}
|
||||||
const firstCase = root.querySelector('app-case-card');
|
});
|
||||||
|
|
||||||
expect(metrics).toBeTruthy();
|
it('renders pitch in both locales with a single h1', async () => {
|
||||||
expect(map).toBeTruthy();
|
TestBed.configureTestingModule({
|
||||||
expect(firstCase).toBeTruthy();
|
providers: [provideRouter(routes), { provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA }],
|
||||||
|
});
|
||||||
|
|
||||||
const position = metrics!.compareDocumentPosition(map!);
|
const harness = await RouterTestingHarness.create();
|
||||||
expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
||||||
const afterMap = map!.compareDocumentPosition(firstCase!);
|
|
||||||
expect(afterMap & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
||||||
|
|
||||||
|
for (const path of ['/pitch', '/en/pitch']) {
|
||||||
|
await harness.navigateByUrl(path);
|
||||||
|
const root = harness.routeNativeElement as HTMLElement;
|
||||||
|
expect(root.querySelectorAll('h1')).toHaveLength(1);
|
||||||
assertPageSemantics(root);
|
assertPageSemantics(root);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,51 @@ describe('ContactBriefing', () => {
|
|||||||
expect(xhrSpy).not.toHaveBeenCalled();
|
expect(xhrSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats project type as optional and enables mailto with name, email and situation', async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ContactBriefing],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ContactBriefing);
|
||||||
|
const copy = SITE_CONTENT_DATA.de.contact;
|
||||||
|
fixture.componentRef.setInput('copy', copy);
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const root = fixture.nativeElement as HTMLElement;
|
||||||
|
const projectType = copy.fields.find((field) => field.id === 'projectType');
|
||||||
|
expect(projectType?.required).toBe(false);
|
||||||
|
expect(projectType?.options?.some((option) => option.value === 'unsure')).toBe(true);
|
||||||
|
expect(
|
||||||
|
SITE_CONTENT_DATA.en.contact.fields
|
||||||
|
.find((field) => field.id === 'projectType')
|
||||||
|
?.options?.some((option) => option.value === 'unsure'),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
const unsure = projectType?.options?.find((option) => option.value === 'unsure');
|
||||||
|
expect(unsure?.label).toBe('Noch unklar');
|
||||||
|
expect(
|
||||||
|
SITE_CONTENT_DATA.en.contact.fields
|
||||||
|
.find((field) => field.id === 'projectType')
|
||||||
|
?.options?.find((option) => option.value === 'unsure')?.label,
|
||||||
|
).toBe('Not sure yet');
|
||||||
|
|
||||||
|
setControl(root, 'contact-name', 'Ada');
|
||||||
|
setControl(root, 'contact-email', 'ada@example.com');
|
||||||
|
setControl(root, 'contact-situation', 'Need a shop.');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const submit = root.querySelector<HTMLAnchorElement>('a.submit');
|
||||||
|
expect(submit?.getAttribute('href')?.startsWith('mailto:')).toBe(true);
|
||||||
|
expect(submit?.getAttribute('href')).not.toContain(
|
||||||
|
encodeURIComponent(`${projectType!.label}:`),
|
||||||
|
);
|
||||||
|
|
||||||
|
setControl(root, 'contact-projectType', 'unsure');
|
||||||
|
fixture.detectChanges();
|
||||||
|
const withType = root.querySelector<HTMLAnchorElement>('a.submit')?.getAttribute('href') ?? '';
|
||||||
|
expect(decodeURIComponent(withType)).toContain(`${projectType!.label}: ${unsure!.label}`);
|
||||||
|
});
|
||||||
|
|
||||||
it('marks required fields invalid only after interaction', async () => {
|
it('marks required fields invalid only after interaction', async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ContactBriefing],
|
imports: [ContactBriefing],
|
||||||
|
|||||||
Reference in New Issue
Block a user