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
2 changes: 1 addition & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fi
STAGED_FILES="$(mktemp)"
trap 'rm -f "$STAGED_FILES"' EXIT

git diff --cached --name-only --diff-filter=ACMR > "$STAGED_FILES"
git diff --cached --name-only --diff-filter=ACMRD > "$STAGED_FILES"

if [ ! -s "$STAGED_FILES" ]; then
echo "[pre-commit] No staged files. Skipping checks."
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/angular-upgrade-visual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version-file: .nvmrc
Expand Down Expand Up @@ -59,11 +61,17 @@ jobs:
PLAYWRIGHT_WEB_SERVER: '1'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0
- name: Checkout immutable Angular 17 reference
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
ref: 68d4939b049e266c1f68efc6b0cf0b94a8854274
path: angular17-reference
- name: Apply the intentional interview hub navigation to the reference
run: |
git -C angular17-reference apply --check ../frontend/e2e/reference-patches/interview-hub-navigation.patch
git -C angular17-reference apply ../frontend/e2e/reference-patches/interview-hub-navigation.patch
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: 20.19.6
Expand All @@ -73,7 +81,7 @@ jobs:
cp frontend/e2e/upgrade-visual.spec.ts frontend/e2e/interview-mocks.ts angular17-reference/frontend/e2e/
cd angular17-reference/frontend
npx playwright install --with-deps chromium
- name: Capture reference using the original application
- name: Capture Angular 17 reference with the same intended navigation
working-directory: angular17-reference/frontend
env:
PLAYWRIGHT_PORT: '4251'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/fullstack-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lemonsqueezy-test-checkout.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Reject production target
run: |
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
Expand Down Expand Up @@ -93,6 +95,11 @@ jobs:
env:
CI: true

- name: Run production incident content and interaction contracts
run: npm run test:e2e:incidents:prod
env:
CI: true

- name: Upload Playwright artifacts (on failure)
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
Expand All @@ -119,6 +126,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
Expand Down Expand Up @@ -179,6 +188,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
Expand Down Expand Up @@ -259,6 +270,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/seo-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ jobs:
prerender-sitemap-guard:
name: SEO Prerender Guard
runs-on: ubuntu-latest
timeout-minutes: 10
# PR merge commits can require a full semantic-history backfill before prerendering.
timeout-minutes: 20
defaults:
run:
working-directory: frontend

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
Expand Down
2 changes: 1 addition & 1 deletion cdn/data-version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"dataVersion": "df2deae26a65"
"dataVersion": "b6eeb5c8fd50"
}
9 changes: 9 additions & 0 deletions docs/references/angular21-upgrade-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ scenarios and Playwright version as the candidate, then compares the candidate
without `--update-snapshots`. The two runs share the same runner and font setup.
Do not update candidate snapshots to hide migration differences.

The interview-hub navigation introduced by `6becd398115e8f25e674e06e87527f371ad457f9`
is an intentional product change after the migration. CI applies the reviewed
`frontend/e2e/reference-patches/interview-hub-navigation.patch` to the pinned
Angular 17 source before capturing its reference. The patch contains only the
header navigation changes and their responsive spacing; it preserves the
reference's Angular 17 component metadata, imports, dependencies and remaining
application. Screenshot thresholds and candidate-update restrictions remain
unchanged. Future product changes require separate review of any reference patch.

## Angular 17 baseline

- Production build and 615 prerender routes: passed.
Expand Down
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ yarn-error.log

# Miscellaneous
/.angular/cache
/.angular/seo/
/src/app/generated/seo-content-dates.ts
/.angular/cdn-assets/
.sass-cache/
/connect.lock
Expand All @@ -43,6 +45,7 @@ testem.log
/test-results/
/e2e/*-snapshots/
!/e2e/upgrade-visual.spec.ts-snapshots/
!/e2e/incident-visual.spec.ts-snapshots/

# System files
.DS_Store
Expand Down
75 changes: 75 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,24 @@ Or run locally with a prerendered build:
2) Serve `dist/frontendatlas/browser` with a static server (any tool you prefer).
3) `PLAYWRIGHT_BASE_URL=http://localhost:4200 PLAYWRIGHT_SSR=1 npx playwright test e2e/seo-ssr.spec.ts`

### Incident content and UI contracts

Build first with `npm run build:prod`, then run `npm run test:e2e:incidents:prod`.
The suite checks every public incident's real prerendered educational markup,
Premium solution exclusion, saved-session hydration, scoring, navigation, and
keyboard accessibility. Backend requests are mocked. This suite also runs in
the Playwright CI job against the production output.

On macOS, `npm run test:e2e:incidents:visual` compares the same production output
with 93 Chromium reference images captured before the renderer change
(application revision `28a133614deb`). It covers every free incident at 390/1440px
and the full Stale Search Race flow at 360/390/834/1366/1440px, with zero differing
pixels allowed. The visual suite is opt-in (`INCIDENT_VISUAL=1`) and skips other
operating systems because the committed references use macOS font rendering.
Do not use `--update-snapshots` to verify a rendering change: investigate the
diff first. New platform references must come from a known-good application
using the same Chromium and font environment as the candidate.

## Draft versioning

To safely handle “CDN updates a question (same id) while users have local drafts”, drafts are versioned by content. See `frontend/docs/draft-versioning.md`.
Expand All @@ -208,6 +226,63 @@ Notes:
- Apply targeted rewrites only for private CSR paths (for this repo: `/dashboard`, `/profile`, `/admin/*`, `/billing/*`, `/onboarding/*`, premium `/tracks/:slug`, premium `/companies/:slug/*`).
- Keep unknown URLs as real `404` responses.

## Sitemap content dates

`npm run gen:seo` generates sitemap XML under `.angular/seo/sitemaps/` and the
ignored Angular module `src/app/generated/seo-content-dates.ts`. These artifacts
are generated before builds and tests, and are not committed. Angular copies only
the XML files to the public build; Git provenance remains in
`.angular/seo/content-dates.json` on the build machine.
The generated date module is excluded from `data-version` hashing, so assigning
commit dates does not invalidate user drafts or require another content commit.

Dates come from the last meaningful content transition in the target commit's
first-parent Git history, using the committer's UTC day. A merge counts when its
content enters that branch; a revert is also a content update. Build time, file
mtime and editorial `updatedAt` fields do not determine SEO dates. Existing
publication and editorial dates remain separate from `dateModified`.

The inventory selects individual catalog objects, article content and explicitly
mapped page dependencies. It ignores date fields, generated files, tests,
comments and visual styles. Adding a public route requires a content mapping.
When changing the meaning of a projection, increment `PROJECTION_VERSION` and
rebuild the historical baseline from Git; do not stamp the current date. A build
with an older projection checkpoint recomputes dates from full Git history
without rewriting the checkpoint.

- Generate: `npm run gen:seo` (also part of `gen:data`, install and test commands).
- Verify existing output without writes: `npm run check:seo-dates`.
- Validate staged sources without writes or staging: `node scripts/generate-seo-content-dates.mjs --staged`.
- Rebuild the historical checkpoint: `node scripts/seo-content-history.mjs --baseline --target <existing-commit>`.
- Regression tests: `npm run test:seo-dates`.

The tracked `scripts/seo-content-baseline.json` is a historical checkpoint, not an
always-current generated manifest. Review its source commits and date changes
when deliberately refreshing it. The normal build resolves later transitions
without changing this file. Unknown historical dates are omitted with a reason.

Local uncommitted content is reported as pending and has no published lastmod.
CI and Vercel builds reject pending semantic content. `--strict` enables that
check explicitly for local generation. The pre-commit check validates the staged
snapshot; it never treats unstaged files as staged or assigns a publication date
before the commit exists.

A Git checkout is required. CI checks out full history; `ensure-seo-history.mjs`
completes a shallow checkout from its existing `origin` using existing credentials.
For [Vercel's shallow checkout](https://vercel.com/kb/guide/how-do-i-use-the-ignored-build-step-field-on-vercel)
without `origin`, it fetches the checked-out commit directly from GitHub using the
validated `VERCEL_GIT_PROVIDER`, `VERCEL_GIT_REPO_OWNER` and `VERCEL_GIT_REPO_SLUG`
[system metadata](https://vercel.com/docs/environment-variables/system-environment-variables).
It does not add remotes or change credentials. Private repositories need an
authenticated `origin` or a full Git checkout; inaccessible history still fails.
`node scripts/ensure-seo-history.mjs --check` checks without fetching. Missing Git,
unavailable history, invalid dates or missing content mappings fail explicitly;
there is no build-time date fallback. Verify this prerequisite in a Vercel preview
before releasing a change to the deployment pipeline.

After building, `npm run seo:meta-check` verifies sitemap/JSON-LD date agreement,
and `npm run seo:link-equity` verifies public URL reachability and inclusion.

## Further help

To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
6 changes: 3 additions & 3 deletions frontend/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"src/llms.txt",
{
"glob": "sitemap*.xml",
"input": "src",
"input": ".angular/seo/sitemaps",
"output": "/"
},
{
Expand Down Expand Up @@ -217,7 +217,7 @@
"src/llms.txt",
{
"glob": "sitemap*.xml",
"input": "src",
"input": ".angular/seo/sitemaps",
"output": "/"
},
{
Expand Down Expand Up @@ -337,7 +337,7 @@
"src/robots.txt",
{
"glob": "sitemap*.xml",
"input": "src",
"input": ".angular/seo/sitemaps",
"output": "/"
},
{
Expand Down
7 changes: 7 additions & 0 deletions frontend/e2e/accessibility.smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ test.describe('accessibility smoke', () => {
name: 'Modal opens visually but fails screen-reader users',
})).toBeVisible();

// The prerendered heading is visible before the simulator is interactive.
// Browser progress is written when the incident component has mounted.
await expect.poll(() => page.evaluate(() => {
const progress = JSON.parse(localStorage.getItem('fa:practice:progress:v3:guest') || '{}');
return progress['incident:modal-screen-reader-failure']?.started === true;
})).toBe(true);

await page.getByRole('button', { name: 'Begin simulator' }).click();

const group = page.getByRole('radiogroup', { name: 'What is actually broken here?' });
Expand Down
9 changes: 9 additions & 0 deletions frontend/e2e/filtering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,13 @@ test('coding list filters sync to URL and persist on reload + back', async ({ pa
await page.goto('/coding');
await expect(page.getByTestId('coding-list-page')).toBeVisible();
await expect(page.getByLabel('Search questions', { exact: true })).toBeVisible();
const technologyLinks = page.getByTestId('coding-tech-question-hubs').getByRole('link');
await expect(technologyLinks).toHaveCount(7);

await page.getByTestId('filter-tech-javascript').click();
await expect(page).toHaveURL(/tech=javascript/);
await expect(technologyLinks).toHaveCount(1);
await expect(technologyLinks).toHaveAttribute('href', '/javascript/interview-questions');
await page.getByTestId(`filter-difficulty-${jsFilters.difficulty}`).click();
await page.getByTestId(`filter-importance-${jsFilters.importance}`).click();
await page.getByTestId('coding-list-search').fill(JS_QUESTION.title);
Expand All @@ -109,6 +114,8 @@ test('coding list filters sync to URL and persist on reload + back', async ({ pa

await page.reload();
await expect(page.getByTestId('coding-list-page')).toBeVisible();
await expect(technologyLinks).toHaveCount(1);
await expect(technologyLinks).toHaveAttribute('href', '/javascript/interview-questions');

await expect(page.getByTestId('coding-list-search')).toHaveValue(JS_QUESTION.title);
await expect(page.getByTestId('filter-tech-javascript')).toHaveClass(/is-active/);
Expand All @@ -124,6 +131,8 @@ test('coding list filters sync to URL and persist on reload + back', async ({ pa

await page.goBack();
await expect(page.getByTestId('coding-list-page')).toBeVisible();
await expect(technologyLinks).toHaveCount(1);
await expect(technologyLinks).toHaveAttribute('href', '/javascript/interview-questions');

await expect(page).toHaveURL(/tech=javascript/);
await expect(page).toHaveURL(new RegExp(`diff=${jsFilters.difficulty}`));
Expand Down
Loading
Loading