diff --git a/.github/workflows/pr-e2e-mvp.yml b/.github/workflows/pr-e2e-mvp.yml index 8e6c88bb..161b525d 100644 --- a/.github/workflows/pr-e2e-mvp.yml +++ b/.github/workflows/pr-e2e-mvp.yml @@ -31,10 +31,20 @@ jobs: cache: 'npm' cache-dependency-path: tests/e2e/package-lock.json - - name: Install LibreOffice + - 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 + 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 @@ -45,9 +55,6 @@ jobs: npm install npx playwright install --with-deps chromium - - name: Generate fixtures - run: node tests/e2e/scripts/generate-fixtures.mjs - - name: Start fixture server run: | cd tests/e2e/fixtures diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore index 945fcd0d..f3b87092 100644 --- a/tests/e2e/.gitignore +++ b/tests/e2e/.gitignore @@ -1,3 +1,9 @@ node_modules/ playwright-report/ test-results/ + +__pycache__/ +fixtures/zip-tmp/ +fixtures/sample.docx +fixtures/sample.xlsx +fixtures/sample.pptx diff --git a/tests/e2e/README.md b/tests/e2e/README.md index d3861d6e..593fa4dd 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -5,6 +5,8 @@ This folder contains a first MVP of end-to-end automated tests. ## What is covered - Basic preview smoke checks for common file types (txt/md/json/xml/csv/html/png) +- Office Phase-2 smoke checks (docx/xlsx/pptx) +- Archive smoke check (zip) - Basic endpoint reachability - Security regression checks for blocked internal-network hosts (`10.*`) on: - `/onlinePreview` @@ -24,13 +26,16 @@ mvn -q -pl server -DskipTests package cd tests/e2e npm install npx playwright install --with-deps chromium +pip3 install -r requirements.txt ``` +> Prerequisite: ensure `zip` command is available in PATH (used for `sample.zip` fixture generation). + 3. Generate fixtures and start fixture server: ```bash cd /path/to/kkFileView -node tests/e2e/scripts/generate-fixtures.mjs +npm run gen:all cd tests/e2e/fixtures && python3 -m http.server 18080 ``` diff --git a/tests/e2e/fixtures/sample.zip b/tests/e2e/fixtures/sample.zip new file mode 100644 index 00000000..00235853 Binary files /dev/null and b/tests/e2e/fixtures/sample.zip differ diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 49cb525c..6e08946d 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -4,9 +4,12 @@ "version": "0.1.0", "type": "module", "scripts": { + "gen:fixtures": "node ./scripts/generate-fixtures.mjs", + "gen:office": "python3 ./scripts/generate-office-fixtures.py", + "gen:all": "npm run gen:fixtures && npm run gen:office", + "pretest": "npm run gen:all", "test": "playwright test", - "test:headed": "playwright test --headed", - "gen:fixtures": "node ./scripts/generate-fixtures.mjs" + "test:headed": "playwright test --headed" }, "devDependencies": { "@playwright/test": "^1.55.0" diff --git a/tests/e2e/requirements.txt b/tests/e2e/requirements.txt new file mode 100644 index 00000000..663606bb --- /dev/null +++ b/tests/e2e/requirements.txt @@ -0,0 +1,3 @@ +python-docx==1.1.2 +openpyxl==3.1.5 +python-pptx==1.0.2 diff --git a/tests/e2e/scripts/generate-fixtures.mjs b/tests/e2e/scripts/generate-fixtures.mjs index e43b5d8c..afd108cb 100644 --- a/tests/e2e/scripts/generate-fixtures.mjs +++ b/tests/e2e/scripts/generate-fixtures.mjs @@ -1,7 +1,10 @@ import fs from 'node:fs'; import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; -const fixturesDir = path.resolve(process.cwd(), 'tests/e2e/fixtures'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixturesDir = path.resolve(__dirname, '..', 'fixtures'); fs.mkdirSync(fixturesDir, { recursive: true }); const write = (name, content) => fs.writeFileSync(path.join(fixturesDir, name), content); @@ -13,6 +16,22 @@ write('sample.xml', 'kkFileViewtrue'); write('sample.csv', 'name,value\nkkFileView,1\ne2e,1\n'); write('sample.html', '

kkFileView fixture

'); +// zip (contains txt) - only generate if missing to avoid noisy local diffs +const zipPath = path.join(fixturesDir, 'sample.zip'); +if (!fs.existsSync(zipPath)) { + const zipWork = path.join(fixturesDir, 'zip-tmp'); + fs.mkdirSync(zipWork, { recursive: true }); + fs.writeFileSync(path.join(zipWork, 'inner.txt'), 'kkFileView zip inner file'); + try { + execFileSync('zip', ['-X', '-q', '-r', zipPath, 'inner.txt'], { cwd: zipWork }); + } catch (err) { + console.error('Failed to create sample.zip fixture. Ensure "zip" is installed and available in PATH.'); + throw err instanceof Error ? err : new Error(String(err)); + } finally { + fs.rmSync(zipWork, { recursive: true, force: true }); + } +} + // 1x1 png write( 'sample.png', diff --git a/tests/e2e/scripts/generate-office-fixtures.py b/tests/e2e/scripts/generate-office-fixtures.py new file mode 100644 index 00000000..d5c2c840 --- /dev/null +++ b/tests/e2e/scripts/generate-office-fixtures.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +from pathlib import Path + +from docx import Document +from openpyxl import Workbook +from pptx import Presentation + +fixtures = Path(__file__).resolve().parent.parent / "fixtures" +fixtures.mkdir(parents=True, exist_ok=True) + +# DOCX +_doc = Document() +_doc.add_heading("kkFileView E2E", level=1) +_doc.add_paragraph("This is a DOCX fixture for Phase-2 E2E.") +_doc.save(fixtures / "sample.docx") + +# XLSX +_wb = Workbook() +_ws = _wb.active +_ws.title = "Sheet1" +_ws["A1"] = "name" +_ws["B1"] = "value" +_ws["A2"] = "kkFileView" +_ws["B2"] = 2 +_wb.save(fixtures / "sample.xlsx") + +# PPTX +_prs = Presentation() +slide_layout = _prs.slide_layouts[1] +slide = _prs.slides.add_slide(slide_layout) +slide.shapes.title.text = "kkFileView E2E" +slide.placeholders[1].text = "This is a PPTX fixture for Phase-2 E2E." +_prs.save(fixtures / "sample.pptx") + +print("office fixtures generated in", fixtures) diff --git a/tests/e2e/specs/preview-smoke.spec.ts b/tests/e2e/specs/preview-smoke.spec.ts index df26d16d..8f2b36c5 100644 --- a/tests/e2e/specs/preview-smoke.spec.ts +++ b/tests/e2e/specs/preview-smoke.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, request as playwrightRequest } from '@playwright/test'; const fixtureBase = process.env.FIXTURE_BASE_URL || 'http://127.0.0.1:18080'; @@ -11,6 +11,32 @@ async function openPreview(request: any, fileUrl: string) { return request.get(`/onlinePreview?url=${encoded}`); } +test.beforeAll(async () => { + const api = await playwrightRequest.newContext(); + const required = [ + 'sample.txt', + 'sample.md', + 'sample.json', + 'sample.xml', + 'sample.csv', + 'sample.html', + 'sample.png', + 'sample.docx', + 'sample.xlsx', + 'sample.pptx', + 'sample.zip', + ]; + + 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('01 home/index reachable', async ({ request }) => { const resp = await request.get('/'); expect(resp.status()).toBeLessThan(500); @@ -51,13 +77,33 @@ test('08 png preview', async ({ request }) => { expect(resp.status()).toBe(200); }); -test('09 security: block 10.x host in onlinePreview', async ({ request }) => { +test('09 docx preview', async ({ request }) => { + const resp = await openPreview(request, `${fixtureBase}/sample.docx`); + expect(resp.status()).toBe(200); +}); + +test('10 xlsx preview', async ({ request }) => { + const resp = await openPreview(request, `${fixtureBase}/sample.xlsx`); + expect(resp.status()).toBe(200); +}); + +test('11 pptx preview', async ({ request }) => { + const resp = await openPreview(request, `${fixtureBase}/sample.pptx`); + expect(resp.status()).toBe(200); +}); + +test('12 zip preview', async ({ request }) => { + const resp = await openPreview(request, `${fixtureBase}/sample.zip`); + expect(resp.status()).toBe(200); +}); + +test('13 security: block 10.x host in onlinePreview', async ({ request }) => { const resp = await openPreview(request, `http://10.1.2.3/a.pdf`); const body = await resp.text(); expect(body).toContain('不受信任'); }); -test('10 security: block 10.x host in getCorsFile', async ({ request }) => { +test('14 security: block 10.x host in getCorsFile', async ({ request }) => { const encoded = b64('http://10.1.2.3/a.pdf'); const resp = await request.get(`/getCorsFile?urlPath=${encoded}`); const body = await resp.text();