diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..5123e5211 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,107 @@ +name: Test + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: 0.163.3 + extended: true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Load documentation content + run: make prepare + + - name: Build site + run: make production-build + + - name: Upload build output + uses: actions/upload-artifact@v4 + if: always() + with: + name: build-output + path: public/ + retention-days: 5 + + test: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + - name: Set up Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: 0.163.3 + extended: true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Load documentation content + run: make prepare + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Check for trailing whitespace + run: | + if grep -r '[[:space:]]$' --include='*.md' --include='*.toml' --include='*.yaml' --include='*.yml' .; then + echo "❌ Found trailing whitespace" + exit 1 + fi + echo "✅ No trailing whitespace" + + - name: Validate YAML files + run: | + for file in .github/workflows/*.yml; do + echo "Checking $file..." + if ! python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null; then + echo "❌ Invalid YAML: $file" + exit 1 + fi + done + echo "✅ All YAML files valid" diff --git a/.gitignore b/.gitignore index c23fd50ca..b2edc058e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,9 @@ node_modules/ public*/ resources/ -# Content from goharbor/harbor repo +# Content from goharbor/harbor repo (generated at build time) content/docs/ +content/cli-docs/ generated/ # Link checker artifacts @@ -46,3 +47,8 @@ Thumbs.db *.un~ .idea/ .hugo_build.lock + +# Playwright test artifacts # +###################### +test-results/ +playwright-report/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..8aaf841e4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM debian:bookworm-slim + +# Install dependencies +RUN apt-get update && apt-get install -y \ + wget \ + ca-certificates \ + nodejs \ + npm && \ + rm -rf /var/lib/apt/lists/* + +# Install Hugo 0.163.3 extended +RUN wget -q https://github.com/gohugoio/hugo/releases/download/v0.163.3/hugo_extended_0.163.3_linux-amd64.tar.gz && \ + tar xzf hugo_extended_0.163.3_linux-amd64.tar.gz -C /usr/local/bin && \ + rm hugo_extended_0.163.3_linux-amd64.tar.gz && \ + hugo version + +WORKDIR /site + +# Copy project files +COPY . /site/ + +# Install npm dependencies +RUN npm install + +# Expose Hugo server port +EXPOSE 1313 + +# Default command: run Hugo server +CMD ["hugo", "server", "--bind", "0.0.0.0", "--buildDrafts", "--buildFuture", "--disableFastRender"] diff --git a/Makefile b/Makefile index 62e4e6dee..b072524e1 100644 --- a/Makefile +++ b/Makefile @@ -38,3 +38,9 @@ check-internal-links: clean build link-checker-setup run-checker check-all-links: clean build link-checker-setup bin/htmltest --conf .htmltest.external.yml + +test: + npm run test:e2e + +test-ui: + npm run test:e2e:ui diff --git a/README.md b/README.md index 92c13fe5b..90d2e539a 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,28 @@ make serve This starts up the local Hugo server on http://localhost:1313. As you make changes, the site refreshes automatically in your browser. +## Testing + +### Running E2E Tests + +The website includes Playwright E2E tests that validate rendering and functionality: + +```sh +make test +``` + +To run tests in interactive UI mode: + +```sh +make test-ui +``` + +Tests are located in the [`e2e/`](./e2e) directory and validate: +- Admonition shortcodes render correctly +- Custom output formats (e.g., `_redirects` for Netlify) +- CSS pipeline and styling +- Site configuration changes + ## Checking links To run the link checker for the Harbor website: diff --git a/config.toml b/config.toml index b00764458..e7f989324 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,6 @@ title = "Harbor" baseURL = "https://goharbor.io" -disableKinds = ["taxonomy", "taxonomyTerm"] +disableKinds = ["taxonomy", "term"] ignoreFiles = ["README.md"] [params] @@ -163,7 +163,7 @@ weight = 4 home = [ "HTML", "REDIRECTS" ] [mediaTypes."text/netlify"] -delimiter = "" +suffixes = [] [outputFormats.REDIRECTS] mediaType = "text/netlify" diff --git a/e2e/breaking-changes.spec.ts b/e2e/breaking-changes.spec.ts new file mode 100644 index 000000000..9c889a931 --- /dev/null +++ b/e2e/breaking-changes.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +test.describe('Hugo breaking changes are fixed', () => { + test('admonition shortcodes render HTML correctly (markdownify fix)', async ({ page }) => { + // Use a docs page known to have admonitions + await page.goto('/docs/2.1.0/administration/configuring-replication/'); + + // Check that admonition elements exist + const admonitions = page.locator('.admonition'); + await expect(admonitions).not.toHaveCount(0); + + // Verify content is rendered, not raw markdown + const firstAdmonition = admonitions.first().locator('.content'); + const innerHTML = await firstAdmonition.innerHTML(); + + // If markdownify failed, we'd see raw markdown syntax like `**bold**` or `[link](url)` + expect(innerHTML).not.toMatch(/\*\*/); + expect(innerHTML).not.toMatch(/\[.*\]\(.*\)/); + + // Verify there's actual rendered text + expect(innerHTML.length).toBeGreaterThan(0); + }); + + test('disableKinds "term" produces no taxonomy pages', async () => { + // The disableKinds config prevents taxonomy/term pages from being built. + // Verify the built output has no taxonomy index pages. + const publicDir = path.join(process.cwd(), 'public'); + + const taxonomyDir = path.join(publicDir, 'tags'); + const termDir = path.join(publicDir, 'categories'); + + expect(fs.existsSync(taxonomyDir)).toBe(false); + expect(fs.existsSync(termDir)).toBe(false); + }); + + test('custom output format _redirects is generated', async () => { + const publicDir = path.join(process.cwd(), 'public'); + const redirectsFile = path.join(publicDir, '_redirects'); + + expect(fs.existsSync(redirectsFile)).toBe(true); + + const content = fs.readFileSync(redirectsFile, 'utf-8'); + expect(content.length).toBeGreaterThan(0); + + // Should contain redirect rules (format: /source /destination 301) + expect(content).toMatch(/^\/.+\s+/m); + }); + + test('CSS pipeline processes without errors (css.Sass)', async ({ page }) => { + await page.goto('/'); + + // Verify stylesheet link exists in head (link elements are never "visible" in Playwright) + const styleLink = page.locator('link[rel="stylesheet"]').first(); + await expect(styleLink).toHaveCount(1); + + const href = await styleLink.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href).toContain('.css'); + + // Verify the stylesheet loaded by checking computed styles are applied + const body = page.locator('body'); + const computedStyle = await body.evaluate(el => + window.getComputedStyle(el).backgroundColor + ); + expect(computedStyle).not.toBe(''); + }); + + test('built CSS is fingerprinted and valid', async () => { + // In production build (hugo build), CSS should be fingerprinted + const publicDir = path.join(process.cwd(), 'public'); + const cssDir = path.join(publicDir, 'css'); + + const files = fs.readdirSync(cssDir); + const fingerprinted = files.find(f => f.match(/\.[a-f0-9]{64}\.css$/)); + + expect(fingerprinted).toBeDefined(); + + const content = fs.readFileSync(path.join(cssDir, fingerprinted!), 'utf-8'); + expect(content.length).toBeGreaterThan(100000); + }); +}); diff --git a/layouts/partials/admonition.html b/layouts/partials/admonition.html index dc37b6529..7701006e3 100644 --- a/layouts/partials/admonition.html +++ b/layouts/partials/admonition.html @@ -10,11 +10,11 @@
{{ with .title }}

- {{ . | markdownify }} + {{ . | string | markdownify }}

{{ end }} - {{ .content | markdownify }} + {{ .content | string | markdownify }}
diff --git a/layouts/partials/css.html b/layouts/partials/css.html index 6fded507d..91528a486 100644 --- a/layouts/partials/css.html +++ b/layouts/partials/css.html @@ -1,15 +1,10 @@ -{{- $inServerMode := site.IsServer }} {{- $includePaths := (slice "node_modules") }} {{- $sass := "sass/style.sass" }} {{- $cssOutput := "css/style.css" }} {{- $devOpts := (dict "targetPath" $cssOutput "includePaths" $includePaths "enableSourceMap" true) }} {{- $prodOpts := (dict "targetPath" $cssOutput "includePaths" $includePaths "outputStyle" "compressed") }} -{{- $cssOpts := cond $inServerMode $devOpts $prodOpts }} -{{- $css := resources.Get $sass | resources.ExecuteAsTemplate $sass . | toCSS $cssOpts }} -{{- if $inServerMode }} - -{{- else }} +{{- $cssOpts := $prodOpts }} +{{- $css := resources.Get $sass | resources.ExecuteAsTemplate $sass . | css.Sass $cssOpts }} {{- $prodCss := $css | postCSS | fingerprint }} -{{- end }} \ No newline at end of file diff --git a/layouts/shortcodes/version.html b/layouts/shortcodes/version.html deleted file mode 100644 index 88cfb24dd..000000000 --- a/layouts/shortcodes/version.html +++ /dev/null @@ -1 +0,0 @@ -{{- site.Params.version -}} diff --git a/netlify.toml b/netlify.toml index ec07d7d3b..8df6a8b80 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,7 +3,7 @@ publish = "public" command = "make production-build" [build.environment] -HUGO_VERSION = "0.74.0" +HUGO_VERSION = "0.163.3" [context.deploy-preview] command = "make preview-build" diff --git a/package.json b/package.json index b7d9f23c6..97b4cb6ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,15 @@ { "dependencies": { - "autoprefixer": "^9.7.4", - "bulma": "^0.8.0", - "postcss-cli": "^7.1.2" + "autoprefixer": "^10.5.2", + "bulma": "^1.0.4", + "postcss-cli": "^11.0.1" + }, + "devDependencies": { + "@playwright/test": "^1.48.0" + }, + "scripts": { + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..5601eadae --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:1313', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chromium'] }, + }, + ], + + webServer: { + command: 'hugo server --buildDrafts --buildFuture', + url: 'http://localhost:1313', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +});