diff --git a/.github/workflows/nightly-e2e.yml b/.github/workflows/nightly-e2e.yml new file mode 100644 index 00000000..cff07f1d --- /dev/null +++ b/.github/workflows/nightly-e2e.yml @@ -0,0 +1,129 @@ +name: Nightly E2E Full + +on: + schedule: + - cron: '30 18 * * *' # 02:30 Asia/Shanghai + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e-nightly: + runs-on: ubuntu-latest + timeout-minutes: 50 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: tests/e2e/package-lock.json + + - name: Setup Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install LibreOffice + zip + run: | + sudo apt-get update + sudo apt-get install -y libreoffice zip + + - name: Setup Python deps for office fixtures + run: | + python -m pip install --upgrade pip + pip install -r tests/e2e/requirements.txt + + - name: Build kkFileView + run: mvn -q -pl server -DskipTests package + + - name: Install E2E deps + working-directory: tests/e2e + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Generate E2E fixtures + working-directory: tests/e2e + run: npm run gen:all + + - name: Start fixture server + run: | + cd tests/e2e/fixtures + python3 -m http.server 18080 > /tmp/fixture-server.log 2>&1 & + + - name: Start kkFileView + run: | + JAR_PATH=$(ls server/target/kkFileView-*.jar | head -n 1) + nohup env KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PATH" > /tmp/kkfileview.log 2>&1 & + + - name: Wait for services + run: | + fixture_ready=false + for i in {1..60}; do + if curl -fsS http://127.0.0.1:18080/sample.txt >/dev/null; then + fixture_ready=true + break + fi + sleep 1 + done + if [ "$fixture_ready" != "true" ]; then + echo "Error: fixture server did not become ready within 60 seconds." >&2 + exit 1 + fi + + kkfileview_ready=false + for i in {1..120}; do + if curl -fsS http://127.0.0.1:8012/ >/dev/null; then + kkfileview_ready=true + break + fi + sleep 1 + done + if [ "$kkfileview_ready" != "true" ]; then + echo "Error: kkFileView service did not become ready within 120 seconds." >&2 + exit 1 + fi + + - name: Run smoke suite + working-directory: tests/e2e + env: + KK_BASE_URL: http://127.0.0.1:8012 + FIXTURE_BASE_URL: http://127.0.0.1:18080 + run: npm run test:smoke + + - name: Run perf suite + working-directory: tests/e2e + env: + KK_BASE_URL: http://127.0.0.1:8012 + FIXTURE_BASE_URL: http://127.0.0.1:18080 + E2E_MAX_PREVIEW_MS: 20000 + run: npm run test:perf + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: nightly-playwright-report + path: tests/e2e/playwright-report + + - name: Upload service logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: nightly-e2e-service-logs + path: | + /tmp/kkfileview.log + /tmp/fixture-server.log diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 593fa4dd..03d4f9f9 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -11,6 +11,7 @@ This folder contains a first MVP of end-to-end automated tests. - Security regression checks for blocked internal-network hosts (`10.*`) on: - `/onlinePreview` - `/getCorsFile` +- Basic performance smoke checks (configurable threshold): txt/docx/xlsx preview response time ## Local run @@ -52,3 +53,13 @@ KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PA cd tests/e2e KK_BASE_URL=http://127.0.0.1:8012 FIXTURE_BASE_URL=http://127.0.0.1:18080 npm test ``` + +Optional: + +```bash +# smoke only (self-contained: will auto-generate fixtures) +npm run test:smoke + +# perf smoke (self-contained; default threshold 15000ms) +E2E_MAX_PREVIEW_MS=15000 npm run test:perf +``` diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 6e08946d..e465cc69 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -9,7 +9,11 @@ "gen:all": "npm run gen:fixtures && npm run gen:office", "pretest": "npm run gen:all", "test": "playwright test", - "test:headed": "playwright test --headed" + "test:headed": "playwright test --headed", + "pretest:smoke": "npm run gen:all", + "test:smoke": "playwright test specs/preview-smoke.spec.ts", + "pretest:perf": "npm run gen:all", + "test:perf": "playwright test specs/perf-smoke.spec.ts" }, "devDependencies": { "@playwright/test": "^1.55.0" diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 8504c5a8..87f8a03c 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -4,8 +4,10 @@ export default defineConfig({ testDir: './specs', timeout: 30_000, expect: { timeout: 10_000 }, + retries: process.env.CI ? 1 : 0, reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]], use: { baseURL: process.env.KK_BASE_URL || 'http://127.0.0.1:8012', + trace: 'on-first-retry', }, }); diff --git a/tests/e2e/specs/perf-smoke.spec.ts b/tests/e2e/specs/perf-smoke.spec.ts new file mode 100644 index 00000000..6976ead5 --- /dev/null +++ b/tests/e2e/specs/perf-smoke.spec.ts @@ -0,0 +1,47 @@ +import { test, expect, request as playwrightRequest } from '@playwright/test'; + +const fixtureBase = process.env.FIXTURE_BASE_URL || 'http://127.0.0.1:18080'; +const envMaxMs = Number(process.env.E2E_MAX_PREVIEW_MS); +const maxMs = Number.isFinite(envMaxMs) ? envMaxMs : 15000; + +function b64(v: string): string { + return Buffer.from(v).toString('base64'); +} + +async function timedPreview(request: any, fileUrl: string) { + const started = Date.now(); + const resp = await request.get(`/onlinePreview?url=${b64(fileUrl)}`); + const elapsed = Date.now() - started; + return { resp, elapsed }; +} + +test.beforeAll(async () => { + const api = await playwrightRequest.newContext(); + const required = ['sample.txt', 'sample.docx', 'sample.xlsx']; + try { + for (const name of required) { + const resp = await api.get(`${fixtureBase}/${name}`); + expect(resp.ok(), `fixture missing or unavailable: ${name}`).toBeTruthy(); + } + } finally { + await api.dispose(); + } +}); + +test('perf: txt preview response under threshold', async ({ request }) => { + const { resp, elapsed } = await timedPreview(request, `${fixtureBase}/sample.txt`); + expect(resp.status()).toBe(200); + expect(elapsed).toBeLessThan(maxMs); +}); + +test('perf: docx preview response under threshold', async ({ request }) => { + const { resp, elapsed } = await timedPreview(request, `${fixtureBase}/sample.docx`); + expect(resp.status()).toBe(200); + expect(elapsed).toBeLessThan(maxMs); +}); + +test('perf: xlsx preview response under threshold', async ({ request }) => { + const { resp, elapsed } = await timedPreview(request, `${fixtureBase}/sample.xlsx`); + expect(resp.status()).toBe(200); + expect(elapsed).toBeLessThan(maxMs); +});