This is an automated email from the ASF dual-hosted git repository. imbajin pushed a commit to branch feat/oink-core-platform in repository https://gitbox.apache.org/repos/asf/hugegraph-doc.git
commit a6111e486db4fae873ed7de7bac21551d506de93 Author: dark <[email protected]> AuthorDate: Fri Sep 4 20:28:51 2026 +0800 test(e2e): gate integrated site behavior - fetch every resolved build SHA inside its isolated job - build an AI-enabled fixture from the manifest-derived config - gate shell, AI, Community, ranking, and axe contracts - keep visual captures advisory with the correct theme key --- .github/workflows/hugo.yml | 30 +++++++++++++ tests/e2e/accessibility.spec.js | 15 +++++++ tests/e2e/ai-enabled.yaml | 8 ++++ tests/e2e/ai.spec.js | 77 +++++++++++++++++++++++++++++++++ tests/e2e/package-lock.json | 24 +++++++++++ tests/e2e/package.json | 3 +- tests/e2e/platform.spec.js | 82 ++++++++++++++++++++++++++++++++++++ tests/e2e/playwright.config.js | 23 +++++++--- tests/e2e/visual.spec.js | 5 ++- tests/e2e/workflow-contract.test.cjs | 33 +++++++++++++++ 10 files changed, 292 insertions(+), 8 deletions(-) diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml index 329da8a15..b34fffec9 100644 --- a/.github/workflows/hugo.yml +++ b/.github/workflows/hugo.yml @@ -186,6 +186,13 @@ jobs: - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.13" + - name: Fetch immutable resolved source + env: + RESOLVED_SHA: ${{ matrix.version.sha }} + run: | + [[ "$RESOLVED_SHA" =~ ^[0-9a-f]{40}$ ]] + git fetch --no-tags origin "$RESOLVED_SHA" + git cat-file -e "$RESOLVED_SHA^{commit}" - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version-file: go.mod @@ -282,6 +289,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 permissions: { contents: read } + env: + HUGO_CACHEDIR: /tmp/hugo-cache-${{ github.run_id }}-${{ github.run_attempt }}-e2e steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -292,6 +301,15 @@ jobs: node-version: "24" cache: npm cache-dependency-path: tests/e2e/package-lock.json + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 + with: + go-version-file: go.mod + cache: true + - name: Setup Hugo Extended + uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1 + with: + hugo-version: ${{ env.HUGO_VERSION }} + extended: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: hugegraph-site-${{ needs.prepare.outputs.artifact_prefix }}-${{ github.run_id }}-${{ github.run_attempt }} @@ -301,10 +319,22 @@ jobs: run: | npm ci npx playwright install --with-deps chromium + - name: Build manifest-derived AI fixture + run: | + python3 scripts/versioning.py config \ + --version latest \ + --site-origin http://127.0.0.1:4174/ \ + --historical-origin "$PRODUCTION_ORIGIN" \ + --output "${RUNNER_TEMP}/ai-version-config.json" + hugo \ + --config "hugo.yaml,${RUNNER_TEMP}/ai-version-config.json,tests/e2e/ai-enabled.yaml" \ + --destination "${RUNNER_TEMP}/ai-site" \ + --cleanDestinationDir --gc --minify --environment production - name: Run blocking Chromium contracts working-directory: tests/e2e env: SITE_ROOT: ${{ github.workspace }}/public-site + AI_SITE_ROOT: ${{ runner.temp }}/ai-site EXPECTED_VERSIONS: ${{ needs.prepare.outputs.selection }} run: npm run test:ci - name: Upload E2E report diff --git a/tests/e2e/accessibility.spec.js b/tests/e2e/accessibility.spec.js new file mode 100644 index 000000000..1fe26d894 --- /dev/null +++ b/tests/e2e/accessibility.spec.js @@ -0,0 +1,15 @@ +const { test, expect } = require("@playwright/test"); +const AxeBuilder = require("@axe-core/playwright").default; + +for (const route of ["/docs/", "/cn/docs/", "/community/", "/cn/community/"]) { + test(`axe WCAG 2.2 AA guard ${route}`, async ({ page }) => { + await page.goto(route); + const results = await new AxeBuilder({ page }) + .withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"]) + .analyze(); + const blocking = results.violations.filter((item) => + ["critical", "serious"].includes(item.impact) + ); + expect(blocking).toEqual([]); + }); +} diff --git a/tests/e2e/ai-enabled.yaml b/tests/e2e/ai-enabled.yaml new file mode 100644 index 000000000..a0a12d188 --- /dev/null +++ b/tests/e2e/ai-enabled.yaml @@ -0,0 +1,8 @@ +params: + ai_search: + enabled: true + provider: kapa + website_id: 0b277570-4740-451e-96fa-1e4ac1ac5e88 + source_groups: + en: e2e-source-en + cn: e2e-source-cn diff --git a/tests/e2e/ai.spec.js b/tests/e2e/ai.spec.js new file mode 100644 index 000000000..2fe2ae6ef --- /dev/null +++ b/tests/e2e/ai.spec.js @@ -0,0 +1,77 @@ +const { test, expect } = require("@playwright/test"); + +const AI_ORIGIN = "http://127.0.0.1:4174"; +const mockBundle = ` +(function () { + var queued = window.Kapa && window.Kapa.q ? window.Kapa.q.slice() : []; + window.__kapaCalls = []; + window.Kapa = function (method, value) { + window.__kapaCalls.push([method, value]); + if (method === 'render' && value && value.onRender) value.onRender(); + }; + queued.forEach(function (args) { window.Kapa.apply(null, Array.from(args)); }); +})();`; + +test.beforeEach(async ({}, testInfo) => { + testInfo.skip(!process.env.AI_SITE_ROOT, "AI-enabled fixture was not built"); +}); + +for (const [locale, route, source, language] of [ + ["en", "/docs/", "e2e-source-en", "en"], + ["cn", "/cn/docs/", "e2e-source-cn", "zh"] +]) { + test(`AI tail is click-gated and locale-bound for ${locale}`, async ({ page }) => { + const requests = []; + await page.route("https://widget.kapa.ai/kapa-widget.bundle.js", async (route) => { + requests.push(route.request().url()); + await route.fulfill({ status: 200, contentType: "text/javascript", body: mockBundle }); + }); + await page.goto(AI_ORIGIN + route); + expect(requests).toEqual([]); + const launcher = page.locator(".hg-ask-ai-launcher"); + await expect(launcher).toBeVisible(); + + await page.locator("[data-td-shell-search-open]").first().click(); + const input = page.locator(".td-shell-search__input"); + await input.fill(" server auth "); + const tail = page.locator("[data-hg-ai-search-tail]"); + await expect(tail).toBeVisible(); + await expect(tail.locator("[data-hg-ask-ai]")).toHaveAttribute( + "data-hg-ai-query", "server auth" + ); + await input.fill(""); + await expect(tail).toHaveCount(0); + await input.fill(">theme"); + await expect(tail).toHaveCount(0); + await input.fill("server auth"); + await tail.locator("[data-hg-ask-ai]").click(); + await expect.poll(() => requests.length).toBe(1); + await expect.poll(() => page.evaluate(() => window.__kapaCalls || [])).toContainEqual([ + "setSourceGroupIDs", [source] + ]); + const script = page.locator("script[data-hg-kapa-widget]"); + await expect(script).toHaveAttribute("data-language", language); + await expect(script).toHaveAttribute("data-source-group-ids-include", source); + const calls = await page.evaluate(() => window.__kapaCalls); + expect(calls).toContainEqual([ + "open", { mode: "ai", query: "server auth", submit: true } + ]); + }); +} + +test("AI 500 remains non-blocking and retry issues one fresh request", async ({ page }) => { + let attempts = 0; + await page.route("https://widget.kapa.ai/kapa-widget.bundle.js", async (route) => { + attempts += 1; + if (attempts === 1) await route.fulfill({ status: 500, body: "failed" }); + else await route.fulfill({ status: 200, contentType: "text/javascript", body: mockBundle }); + }); + await page.goto(AI_ORIGIN + "/docs/"); + const launcher = page.locator(".hg-ask-ai-launcher"); + await launcher.dblclick(); + await expect.poll(() => attempts).toBe(1); + await expect(launcher).toHaveAttribute("data-hg-ai-state", "error"); + await launcher.click(); + await expect.poll(() => attempts).toBe(2); + await expect(launcher).toHaveAttribute("data-hg-ai-state", "ready"); +}); diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json index 6b01049ce..569ab0709 100644 --- a/tests/e2e/package-lock.json +++ b/tests/e2e/package-lock.json @@ -6,12 +6,26 @@ "": { "name": "hugegraph-doc-e2e", "devDependencies": { + "@axe-core/playwright": "4.13.0", "@playwright/test": "1.62.1" }, "engines": { "node": ">=24" } }, + "node_modules/@axe-core/playwright": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", + "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.13.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@playwright/test": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", @@ -28,6 +42,16 @@ "node": ">=20" } }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", diff --git a/tests/e2e/package.json b/tests/e2e/package.json index e63f7d9c2..232afcefa 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -6,10 +6,11 @@ }, "scripts": { "test": "playwright test", - "test:ci": "playwright test versioning.spec.js search-ranking.spec.js --reporter=line,html", + "test:ci": "node --test workflow-contract.test.cjs && playwright test versioning.spec.js search-ranking.spec.js platform.spec.js ai.spec.js accessibility.spec.js --reporter=line,html", "test:visual": "playwright test visual.spec.js --reporter=line" }, "devDependencies": { + "@axe-core/playwright": "4.13.0", "@playwright/test": "1.62.1" } } diff --git a/tests/e2e/platform.spec.js b/tests/e2e/platform.spec.js new file mode 100644 index 000000000..da5f0be39 --- /dev/null +++ b/tests/e2e/platform.spec.js @@ -0,0 +1,82 @@ +const { test, expect } = require("@playwright/test"); + +for (const locale of ["en", "cn"]) { + const prefix = locale === "cn" ? "/cn" : ""; + test(`latest ${locale} sidebar persists and isolates collapse`, async ({ page }) => { + await page.goto(`${prefix}/docs/introduction/`); + const toggle = page.locator("[data-td-shell-tree-toggle][aria-controls]").last(); + await toggle.click(); + const target = await toggle.getAttribute("aria-controls"); + const key = `oink.sidebar.v1.latest.${locale}`; + await expect.poll(() => page.evaluate((name) => localStorage.getItem(name), key)) + .toContain(target); + await page.reload(); + await expect(page.locator(`[aria-controls="${target}"]`)).toHaveAttribute( + "aria-expanded", "true" + ); + + await page.locator("[data-td-shell-sidebar-toggle]").first().click(); + await expect(page.locator("#td-shell-sidebar")).toHaveAttribute("aria-hidden", "true"); + await expect(page.locator("#td-shell-sidebar")).toHaveJSProperty("inert", true); + const restore = page.locator(".hg-sidebar-restore"); + await expect(restore).toBeVisible(); + await restore.click(); + await expect(page.locator("#td-shell-sidebar")).not.toHaveAttribute( + "aria-hidden", "true" + ); + }); + + test(`latest ${locale} mobile drawer restores focus and unlocks scroll`, async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto(`${prefix}/docs/`); + const opener = page.locator("[data-td-shell-drawer-open]"); + await opener.click(); + await expect(page.locator("html")).toHaveAttribute("data-td-shell-drawer", "open"); + await page.locator("[data-td-shell-drawer-close]").click(); + await expect(page.locator("#td-shell-sidebar")).toHaveJSProperty("inert", true); + await expect(opener).toBeFocused(); + await expect(page.locator("html")).not.toHaveAttribute("data-td-shell-lock", ""); + }); +} + +test("disabled AI emits no UI or Kapa request", async ({ page }) => { + const kapaRequests = []; + page.on("request", (request) => { + if (request.url().includes("kapa.ai")) kapaRequests.push(request.url()); + }); + await page.goto("/docs/"); + await page.locator("[data-td-shell-search-open]").first().click(); + await page.locator(".td-shell-search__input").fill("server"); + await expect(page.locator('[role="option"]').first()).toBeVisible(); + expect(kapaRequests).toEqual([]); + await expect(page.locator("[data-hg-ask-ai]")).toHaveCount(0); +}); + +test("Community grid and HTML/Print/Markdown profiles stay in parity", async ({ + page, + request +}) => { + for (const [width, columns] of [[1440, 5], [900, 3], [390, 2], [320, 2]]) { + await page.setViewportSize({ width, height: 900 }); + await page.goto("/community/"); + const grid = page.locator(".hg-community-members__grid").first(); + await expect(grid).toBeVisible(); + expect( + await grid.evaluate((node) => getComputedStyle(node).gridTemplateColumns.split(" ").length) + ).toBe(columns); + } + await page.evaluate(() => localStorage.setItem("td-color-theme", "dark")); + await page.reload(); + await expect(page.locator(".hg-community-member__link").first()).toBeVisible(); + await expect(page.locator(".hg-community-member__initials").first()).toBeAttached(); + + const htmlProfiles = await page + .locator("#project-members .hg-community-member__link") + .evaluateAll((links) => links.map((link) => link.href).sort()); + const print = await (await request.get("/_print/community/")).text(); + const markdown = await (await request.get("/community/index.md")).text(); + for (const profile of htmlProfiles) { + expect(print).toContain(profile); + expect(markdown).toContain(profile); + } +}); diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 8a1052461..63cd092d0 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -1,6 +1,7 @@ const { defineConfig } = require("@playwright/test"); const siteRoot = process.env.SITE_ROOT; +const aiSiteRoot = process.env.AI_SITE_ROOT; if (!siteRoot) { throw new Error("SITE_ROOT must point to an aggregate site artifact"); } @@ -20,10 +21,20 @@ module.exports = defineConfig({ trace: "retain-on-failure", screenshot: "only-on-failure" }, - webServer: { - command: `python3 -m http.server 4173 --bind 127.0.0.1 --directory ${JSON.stringify(siteRoot)}`, - url: "http://127.0.0.1:4173/", - reuseExistingServer: false, - timeout: 30_000 - } + webServer: [ + { + command: `python3 -m http.server 4173 --bind 127.0.0.1 --directory ${JSON.stringify(siteRoot)}`, + url: "http://127.0.0.1:4173/", + reuseExistingServer: false, + timeout: 30_000 + }, + ...(aiSiteRoot + ? [{ + command: `python3 -m http.server 4174 --bind 127.0.0.1 --directory ${JSON.stringify(aiSiteRoot)}`, + url: "http://127.0.0.1:4174/", + reuseExistingServer: false, + timeout: 30_000 + }] + : []) + ] }); diff --git a/tests/e2e/visual.spec.js b/tests/e2e/visual.spec.js index 6b1b680f4..b6fe2a6f3 100644 --- a/tests/e2e/visual.spec.js +++ b/tests/e2e/visual.spec.js @@ -12,7 +12,10 @@ const states = [ for (const [name, url, viewport, theme] of states) { test(`capture advisory ${name}`, async ({ page }) => { await page.setViewportSize(viewport); - await page.addInitScript((value) => localStorage.setItem("td-theme", value), theme); + await page.addInitScript( + (value) => localStorage.setItem("td-color-theme", value), + theme + ); await page.goto(url); await expect(page.locator("body")).toBeVisible(); const directory = path.join(__dirname, "visual-results"); diff --git a/tests/e2e/workflow-contract.test.cjs b/tests/e2e/workflow-contract.test.cjs new file mode 100644 index 000000000..ef43141f1 --- /dev/null +++ b/tests/e2e/workflow-contract.test.cjs @@ -0,0 +1,33 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); + +const workflow = fs.readFileSync( + path.resolve(__dirname, "../../.github/workflows/hugo.yml"), + "utf8" +); + +test("each build fetches and verifies its immutable matrix SHA", () => { + assert.match(workflow, /RESOLVED_SHA: \$\{\{ matrix\.version\.sha \}\}/); + assert.match(workflow, /git fetch --no-tags origin "\$RESOLVED_SHA"/); + assert.match(workflow, /git cat-file -e "\$RESOLVED_SHA\^\{commit\}"/); +}); + +test("only publish receives write permission and deploy stays read-only", () => { + const writeMatches = workflow.match(/contents: write/g) || []; + assert.equal(writeMatches.length, 1); + assert.match( + workflow, + /deploy:[\s\S]*?permissions: \{ contents: read \}[\s\S]*?publish:/ + ); + assert.match(workflow, /publish:[\s\S]*?permissions: \{ contents: write \}/); +}); + +test("event plan fixes origins, branches, confirmations, and five-version order", () => { + assert.match(workflow, /selection="latest,1\.7,1\.5,1\.3,1\.0"/); + assert.match(workflow, /test "\$CONFIRMATION" = "publish asf-staging-oink"/); + assert.match(workflow, /test "\$CONFIRMATION" = "publish asf-site"/); + assert.match(workflow, /production:asf-site\|staging:asf-staging-oink/); + assert.doesNotMatch(workflow, /permissions:\s*write-all/); +});
