Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit 263a3df

Browse files
committed
test(exporter): add Playwright e2e for ZIP export download
Two scenarios in tests/e2e/export-zip.spec.ts: (a) full happy-path — login as admin, open hello-world editor, click Export, select ZIP, submit, poll until status=succeeded, click download, assert .zip filename received; (b) client-side validation — submitting with no target chosen surfaces an inline alert. Filed against the Playwright bootstrap that lands cohort-wide; runs as part of the standard tests/e2e/ project once the runner is wired up.
1 parent b620ef2 commit 263a3df

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

‎tests/e2e/export-zip.spec.ts‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3+
* SPDX-License-Identifier: EUPL-1.2
4+
*
5+
* Playwright end-to-end coverage for the ZIP export flow of spec #9
6+
* (openbuilt-export-to-real-app).
7+
*
8+
* Flow:
9+
* 1. Authenticate as admin against the local Nextcloud dev instance
10+
* (admin:admin per nextcloud-docker-dev defaults).
11+
* 2. Open the hello-world Application editor.
12+
* 3. Click the Export action, choose ZIP target, accept the default
13+
* version + license.
14+
* 4. Submit; expect a 202 + ExportJob UUID surfaced in the jobs list.
15+
* 5. Poll the row until `status=succeeded` (≤ 60 s).
16+
* 6. Click the download button; assert a ZIP file is received with the
17+
* expected filename pattern `<appId>-<version>.zip` (or the job-UUID
18+
* fallback the current ExportService emits).
19+
*
20+
* NOTE: The Playwright runner is not wired up in OpenBuilt yet — this file
21+
* is committed alongside the apply PR per task 7.2 / 8.x of the spec.
22+
* It runs once the cohort-wide Playwright bootstrap lands and asserts the
23+
* end-to-end UX contract the controller + background-job tests have
24+
* already locked at the unit level.
25+
*/
26+
27+
import { test, expect, type Download } from '@playwright/test'
28+
29+
const NEXTCLOUD_URL = process.env.NEXTCLOUD_URL || 'http://localhost:8080'
30+
const ADMIN_USER = process.env.NC_ADMIN_USER || 'admin'
31+
const ADMIN_PASSWORD = process.env.NC_ADMIN_PASSWORD || 'admin'
32+
const APPLICATION_SLUG = 'hello-world'
33+
const POLL_TIMEOUT_MS = 60_000
34+
35+
test.describe('OpenBuilt ZIP export', () => {
36+
test.beforeEach(async ({ page }) => {
37+
// Login via the Nextcloud login form. CI uses storageState; this
38+
// fallback keeps the spec runnable in local dev.
39+
await page.goto(`${NEXTCLOUD_URL}/index.php/login`)
40+
if (await page.locator('input[name="user"]').isVisible({ timeout: 5_000 }).catch(() => false)) {
41+
await page.fill('input[name="user"]', ADMIN_USER)
42+
await page.fill('input[name="password"]', ADMIN_PASSWORD)
43+
await page.locator('button[type="submit"], input[type="submit"]').first().click()
44+
await page.waitForURL(/\/index\.php\/apps\//, { timeout: 15_000 })
45+
}
46+
})
47+
48+
test('export a hello-world Application as a ZIP and download it', async ({ page }) => {
49+
// 1. Navigate to the hello-world editor.
50+
await page.goto(`${NEXTCLOUD_URL}/index.php/apps/openbuilt/applications/${APPLICATION_SLUG}`)
51+
52+
// 2. Open the Export dialog. The button is wired in
53+
// src/views/ApplicationDetail.vue per task 8.3.
54+
const exportButton = page.getByRole('button', { name: /export/i })
55+
await expect(exportButton).toBeVisible({ timeout: 15_000 })
56+
await exportButton.click()
57+
58+
// 3. Choose ZIP target. NcSelect renders a combobox-style trigger;
59+
// the inputLabel prop (nc-input-labels gate) gives screen
60+
// readers the "Target" label we can locate by.
61+
const targetSelect = page.getByRole('combobox', { name: /target/i })
62+
await expect(targetSelect).toBeVisible()
63+
await targetSelect.click()
64+
await page.getByRole('option', { name: /^ZIP/i }).click()
65+
66+
// 4. Submit and capture the UUID surfaced in the row.
67+
await page.getByRole('button', { name: /^submit|^export$/i }).click()
68+
69+
// 5. Poll the jobs list until status=succeeded.
70+
const succeededRow = page.locator('[data-test="export-job-row"]:has-text("succeeded")').first()
71+
await expect(succeededRow).toBeVisible({ timeout: POLL_TIMEOUT_MS })
72+
73+
// 6. Click the download button and capture the resulting file.
74+
const downloadPromise: Promise<Download> = page.waitForEvent('download')
75+
await succeededRow.getByRole('button', { name: /download/i }).click()
76+
const download = await downloadPromise
77+
78+
const suggestedFilename = download.suggestedFilename()
79+
expect(suggestedFilename).toMatch(/\.zip$/i)
80+
// Filename either carries the app slug, the job UUID, or a generic
81+
// `export.zip` — all three are documented in the controller +
82+
// service layer. The .zip extension is the load-bearing assertion.
83+
expect(suggestedFilename.length).toBeGreaterThan(0)
84+
})
85+
86+
test('export dialog rejects submission with invalid target', async ({ page }) => {
87+
// Locks the client-side guard mirror of the 422 controller path.
88+
await page.goto(`${NEXTCLOUD_URL}/index.php/apps/openbuilt/applications/${APPLICATION_SLUG}`)
89+
const exportButton = page.getByRole('button', { name: /export/i })
90+
await expect(exportButton).toBeVisible({ timeout: 15_000 })
91+
await exportButton.click()
92+
93+
// Submit without choosing a target — NcSelect should remain in its
94+
// placeholder state and the submit button should be disabled (or
95+
// produce an inline validation error).
96+
const submitBtn = page.getByRole('button', { name: /^submit|^export$/i })
97+
const isDisabled = await submitBtn.isDisabled().catch(() => false)
98+
if (!isDisabled) {
99+
await submitBtn.click()
100+
// Validation error surfaces as a notice element with role=alert.
101+
await expect(page.getByRole('alert')).toBeVisible({ timeout: 5_000 })
102+
}
103+
})
104+
})

0 commit comments

Comments
 (0)