Constrain UI tests to disposable targets

This commit is contained in:
ms2k 2026-08-07 00:34:48 +09:00
commit 7c4f4ffe0d
3 changed files with 18 additions and 9 deletions

View file

@ -1,6 +1,6 @@
# OPNsense WebGUI integration tests # OPNsense WebGUI integration tests
These Playwright tests use a disposable OPNsense deployment. They create and delete NordVPN enrollment records and require `OPNSENSE_UI_TEST_ALLOW_MUTATION=1`. These Playwright tests use a disposable OPNsense deployment. They create and delete only entries containing the synthetic `UI_TEST_NETWORK` alias and require `OPNSENSE_UI_TEST_ALLOW_MUTATION=1`.
Start a QEMU fixture after building a package. The launcher needs a compatible OPNsense base image and transfer disk, neither of which is stored in this repository. Start a QEMU fixture after building a package. The launcher needs a compatible OPNsense base image and transfer disk, neither of which is stored in this repository.
@ -22,3 +22,5 @@ sh -c 'cd tests/ui && pnpm install --frozen-lockfile && pnpm test'
``` ```
The suite mocks only the countries API response with synthetic values. It covers populated country options, failure-policy labels, native alias selectors, Add/Edit/Clone state restoration, and the selector menu/footer layout. The suite mocks only the countries API response with synthetic values. It covers populated country options, failure-policy labels, native alias selectors, Add/Edit/Clone state restoration, and the selector menu/footer layout.
The browser target must be loopback by default. A remote disposable deployment requires `OPNSENSE_UI_TEST_ALLOW_REMOTE=1` and a trusted TLS certificate.

View file

@ -6,13 +6,20 @@ if (!baseURL) {
throw new Error('Set OPNSENSE_UI_BASE_URL to the disposable OPNsense WebGUI URL.'); throw new Error('Set OPNSENSE_UI_BASE_URL to the disposable OPNsense WebGUI URL.');
} }
const hostname = new URL(baseURL).hostname;
const isLoopback = ['127.0.0.1', '::1', '[::1]', 'localhost'].includes(hostname);
if (!isLoopback && process.env.OPNSENSE_UI_TEST_ALLOW_REMOTE !== '1') {
throw new Error('Set OPNSENSE_UI_TEST_ALLOW_REMOTE=1 to test a remote disposable OPNsense deployment.');
}
module.exports = defineConfig({ module.exports = defineConfig({
testDir: './specs', testDir: './specs',
timeout: 30000, timeout: 30000,
workers: 1, workers: 1,
use: { use: {
baseURL, baseURL,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: isLoopback,
trace: 'retain-on-failure', trace: 'retain-on-failure',
}, },
}); });

View file

@ -50,13 +50,13 @@ test('renders and preserves NordVPN enrollment controls', async ({ page }) => {
await expect(failurePolicy.locator('option[value="fail_open"]')).toHaveText('Use direct egress'); await expect(failurePolicy.locator('option[value="fail_open"]')).toHaveText('Use direct egress');
const grid = page.getByRole('grid'); const grid = page.getByRole('grid');
const deleteButtons = grid.getByRole('button', { name: 'Delete', exact: true }); const testRows = grid.getByRole('row').filter({ hasText: 'UI_TEST_NETWORK' });
while (await deleteButtons.count()) { while (await testRows.count()) {
const before = await deleteButtons.count(); const before = await testRows.count();
await deleteButtons.first().click(); await testRows.first().getByRole('button', { name: 'Delete' }).click();
const removal = page.getByRole('dialog', { name: 'Remove' }); const removal = page.getByRole('dialog', { name: 'Remove' });
await removal.getByRole('button', { name: 'Yes' }).click(); await removal.getByRole('button', { name: 'Yes' }).click();
await expect(deleteButtons).toHaveCount(before - 1); await expect(testRows).toHaveCount(before - 1);
} }
await page.getByRole('button', { name: 'Add', exact: true }).click(); await page.getByRole('button', { name: 'Add', exact: true }).click();
@ -104,12 +104,12 @@ test('renders and preserves NordVPN enrollment controls', async ({ page }) => {
await dialog.getByRole('button', { name: 'Close' }).click(); await dialog.getByRole('button', { name: 'Close' }).click();
await expect(dialog).toBeHidden(); await expect(dialog).toBeHidden();
const beforeDelete = await deleteButtons.count(); const beforeDelete = await testRows.count();
await row.getByRole('button', { name: 'Delete' }).click(); await row.getByRole('button', { name: 'Delete' }).click();
const removal = page.getByRole('dialog', { name: 'Remove' }); const removal = page.getByRole('dialog', { name: 'Remove' });
await expect(removal).toBeVisible(); await expect(removal).toBeVisible();
await removal.getByRole('button', { name: 'Yes' }).click(); await removal.getByRole('button', { name: 'Yes' }).click();
await expect(deleteButtons).toHaveCount(beforeDelete - 1); await expect(testRows).toHaveCount(beforeDelete - 1);
expect(errors).toEqual([]); expect(errors).toEqual([]);
}); });