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/);
+});

Reply via email to