mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-08 02:39:05 +02:00
feat(ux): replace individual save buttons with single "unsaved changes bar" (#1757)
This commit is contained in:
@@ -3,6 +3,7 @@ import { emailVerificationTokens, users } from '../data';
|
||||
import authUtil from '../utils/auth.util';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
@@ -15,11 +16,38 @@ test('Update account details', async ({ page }) => {
|
||||
await page.getByLabel('Display Name').fill('Timothy Apple');
|
||||
await page.getByLabel('Email').fill('timothy.apple@test.com');
|
||||
await page.getByLabel('Username').fill('timothy');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await saveUnsavedChanges(page);
|
||||
});
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Account details updated successfully'
|
||||
);
|
||||
test('Failed account update remains dirty and can be retried', async ({ page }) => {
|
||||
await page.goto('/settings/account');
|
||||
|
||||
let failedUpdates = 0;
|
||||
await page.route('**/api/users/me', async (route) => {
|
||||
if (route.request().method() !== 'PUT' || failedUpdates > 0) {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
failedUpdates++;
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'Temporary account update failure' })
|
||||
});
|
||||
});
|
||||
|
||||
const displayName = page.getByLabel('Display Name');
|
||||
await displayName.fill('Retryable Account');
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.getByText('Temporary account update failure', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Save', exact: true })).toBeVisible();
|
||||
expect(failedUpdates).toBe(1);
|
||||
|
||||
await saveUnsavedChanges(page);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Display Name')).toHaveValue('Retryable Account');
|
||||
});
|
||||
|
||||
test('Update account details fails with already taken email', async ({ page }) => {
|
||||
@@ -27,9 +55,9 @@ test('Update account details fails with already taken email', async ({ page }) =
|
||||
|
||||
await page.getByLabel('Email').fill(users.craig.email);
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Email is already in use');
|
||||
await expect(page.getByText('Email is already in use', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update account details fails with already taken username', async ({ page }) => {
|
||||
@@ -37,9 +65,9 @@ test('Update account details fails with already taken username', async ({ page }
|
||||
|
||||
await page.getByLabel('Username').fill(users.craig.username);
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||
await expect(page.getByText('Username is already in use', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update account details fails with already taken username in different casing', async ({
|
||||
@@ -49,9 +77,9 @@ test('Update account details fails with already taken username in different casi
|
||||
|
||||
await page.getByLabel('Username').fill(users.craig.username.toUpperCase());
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||
await expect(page.getByText('Username is already in use', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Change Locale', async ({ page }) => {
|
||||
|
||||
+4
-11
@@ -3,6 +3,7 @@ import * as jose from 'jose';
|
||||
import { apis, oidcClients } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import * as oidcUtil from '../utils/oidc.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
@@ -69,9 +70,7 @@ test('Edit the name of an API', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/apis/${apis.orders.id}`);
|
||||
|
||||
await page.getByLabel('Name', { exact: true }).fill('Orders API renamed');
|
||||
await page.getByRole('button', { name: 'Save' }).nth(0).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('API updated successfully');
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Name', { exact: true })).toHaveValue('Orders API renamed');
|
||||
@@ -84,11 +83,7 @@ test('Add a permission to an API', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Add another' }).click();
|
||||
await page.getByPlaceholder('Permission', { exact: true }).last().fill('ship:orders');
|
||||
await page.getByPlaceholder('Name', { exact: true }).last().fill('Ship orders');
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Permissions updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
// The two seeded permissions plus the newly added one
|
||||
@@ -259,9 +254,7 @@ test('Allow all metadata document clients for an API', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Metadata document clients' }).click();
|
||||
await page.getByLabel('Allow all metadata document clients').click();
|
||||
await page.getByLabel(apis.orders.permissions.readOrders.name, { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('API access updated successfully');
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole('tab', { name: 'Metadata document clients' }).click();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await cleanupBackend();
|
||||
@@ -13,11 +14,7 @@ test('Update general configuration', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Home Page' }).click();
|
||||
await page.getByRole('option', { name: 'My Apps' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Application configuration updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -28,6 +25,95 @@ test('Update general configuration', async ({ page }) => {
|
||||
await page.waitForURL('/settings/apps');
|
||||
});
|
||||
|
||||
test('Save configuration from every editable tab together', async ({ page }) => {
|
||||
await page.getByLabel('Application Name', { exact: true }).fill('Combined Settings');
|
||||
await page.getByRole('tab', { name: 'User Creation' }).click();
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||
await page.getByRole('option', { name: 'Open Signup' }).click();
|
||||
await page.getByRole('tab', { name: 'Passkeys' }).click();
|
||||
await page.getByRole('button', { name: 'User verification' }).click();
|
||||
await page.getByRole('option', { name: 'Preferred' }).click();
|
||||
await page.getByRole('tab', { name: 'Email' }).click();
|
||||
await page.getByLabel('SMTP Host').fill('smtp.combined.test');
|
||||
await page.getByLabel('SMTP Port').fill('587');
|
||||
await page.getByLabel('SMTP From').fill('combined@example.com');
|
||||
await page.getByRole('tab', { name: 'OIDC' }).click();
|
||||
await page.getByRole('textbox').fill('https://combined.example.com/*');
|
||||
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'General' }).click();
|
||||
await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue(
|
||||
'Combined Settings'
|
||||
);
|
||||
await page.getByRole('tab', { name: 'Email' }).click();
|
||||
await expect(page.getByLabel('SMTP Host')).toHaveValue('smtp.combined.test');
|
||||
await page.getByRole('tab', { name: 'Passkeys' }).click();
|
||||
await expect(page.getByRole('button', { name: 'User verification' })).toContainText('Preferred');
|
||||
await page.getByRole('tab', { name: 'OIDC' }).click();
|
||||
await expect(page.getByRole('textbox')).toHaveValue('https://combined.example.com/*');
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole('tab', { name: 'General' }).click();
|
||||
await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue(
|
||||
'Combined Settings'
|
||||
);
|
||||
await page.getByRole('tab', { name: 'User Creation' }).click();
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||
await expect(page.getByRole('option', { name: 'Open Signup' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('tab', { name: 'Email' }).click();
|
||||
await expect(page.getByLabel('SMTP Host')).toHaveValue('smtp.combined.test');
|
||||
await page.getByRole('tab', { name: 'Passkeys' }).click();
|
||||
await expect(page.getByRole('button', { name: 'User verification' })).toContainText('Preferred');
|
||||
await page.getByRole('tab', { name: 'OIDC' }).click();
|
||||
await expect(page.getByRole('textbox')).toHaveValue('https://combined.example.com/*');
|
||||
});
|
||||
|
||||
test('Invalid hidden configuration prevents every section from being saved', async ({ page }) => {
|
||||
let updateRequests = 0;
|
||||
page.on('request', (request) => {
|
||||
if (
|
||||
request.method() === 'PUT' &&
|
||||
new URL(request.url()).pathname === '/api/application-configuration'
|
||||
) {
|
||||
updateRequests++;
|
||||
}
|
||||
});
|
||||
|
||||
await page.getByLabel('Application Name', { exact: true }).fill('Validated Together');
|
||||
await page.getByRole('tab', { name: 'Email' }).click();
|
||||
await page.getByLabel('SMTP Host').fill('smtp.validation.test');
|
||||
await page.getByLabel('SMTP From').fill('validation@example.com');
|
||||
await page.getByRole('tab', { name: 'General' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
const emailTab = page.getByRole('tab', { name: 'Email' });
|
||||
const smtpPort = page.getByLabel('SMTP Port');
|
||||
await expect(page.getByText('Please fix the highlighted errors before saving')).toBeVisible();
|
||||
await expect(emailTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(smtpPort).toHaveAttribute('aria-invalid', 'true');
|
||||
await expect(smtpPort).toBeFocused();
|
||||
expect(updateRequests).toBe(0);
|
||||
|
||||
await smtpPort.fill('587');
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole('tab', { name: 'General' }).click();
|
||||
await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue(
|
||||
'Validated Together'
|
||||
);
|
||||
await page.getByRole('tab', { name: 'Email' }).click();
|
||||
await expect(page.getByLabel('SMTP Host')).toHaveValue('smtp.validation.test');
|
||||
await expect(page.getByLabel('SMTP Port')).toHaveValue('587');
|
||||
await expect(page.getByLabel('SMTP From')).toHaveValue('validation@example.com');
|
||||
});
|
||||
|
||||
test.describe('Update user creation configuration', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'User Creation' }).click();
|
||||
@@ -37,11 +123,7 @@ test.describe('Update user creation configuration', () => {
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||
await page.getByRole('option', { name: 'Open Signup' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -59,11 +141,7 @@ test.describe('Update user creation configuration', () => {
|
||||
await expect(designersOption).toBeChecked();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -81,11 +159,7 @@ test.describe('Update user creation configuration', () => {
|
||||
await page.getByPlaceholder('Key').nth(1).fill('another-claim');
|
||||
await page.getByPlaceholder('Value').nth(1).fill('another-value');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -114,10 +188,7 @@ test('Update passkey configuration', async ({ page }) => {
|
||||
await expect(allowSyncedPasskeys).toBeChecked();
|
||||
await allowSyncedPasskeys.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Passkey configuration updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
const registrationResponse = await page.request.get('/api/webauthn/register/start');
|
||||
expect(registrationResponse.ok()).toBeTruthy();
|
||||
@@ -157,11 +228,7 @@ test('Update email configuration', async ({ page }) => {
|
||||
await page.getByLabel('Email Login Code from Admin').click();
|
||||
await page.getByLabel('API Key Expiration').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Email configuration updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -176,7 +243,52 @@ test('Update email configuration', async ({ page }) => {
|
||||
await expect(page.getByLabel('API Key Expiration')).toBeChecked();
|
||||
});
|
||||
|
||||
test('Save LDAP configuration while LDAP remains disabled', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'LDAP' }).click();
|
||||
|
||||
const disableButton = page.getByRole('button', { name: 'Disable', exact: true });
|
||||
if (await disableButton.isVisible()) {
|
||||
await disableButton.click();
|
||||
await expect(page.getByRole('button', { name: 'Enable', exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
const softDeleteUsers = page.getByRole('switch', { name: 'Keep disabled users from LDAP' });
|
||||
const originalValue = await softDeleteUsers.isChecked();
|
||||
await softDeleteUsers.click();
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||||
await expect(softDeleteUsers).toBeChecked({ checked: originalValue });
|
||||
|
||||
await softDeleteUsers.click();
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await page.getByRole('tab', { name: 'LDAP' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Enable', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('switch', { name: 'Keep disabled users from LDAP' })).toBeChecked({
|
||||
checked: !originalValue
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Update application images', () => {
|
||||
test('should detect image resets as unsaved changes', async ({ page }) => {
|
||||
await page
|
||||
.getByLabel('Background Image', { exact: true })
|
||||
.setInputFiles('resources/images/clouds.jpg');
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'Reset to default Background Image', exact: true })
|
||||
.click();
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Reset to default Background Image', exact: true })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should upload images and reset custom logos', async ({ page }) => {
|
||||
await page
|
||||
.getByLabel('Favicon', { exact: true })
|
||||
@@ -196,11 +308,7 @@ test.describe('Update application images', () => {
|
||||
await page
|
||||
.getByLabel('Background Image', { exact: true })
|
||||
.setInputFiles('resources/images/clouds.jpg');
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).nth(1).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Images updated successfully. It may take a few minutes to update.'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.request
|
||||
.get('/api/application-images/favicon')
|
||||
@@ -234,15 +342,11 @@ test.describe('Update application images', () => {
|
||||
);
|
||||
})
|
||||
);
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).nth(1).click();
|
||||
await saveUnsavedChanges(page);
|
||||
for (const response of await Promise.all(logoDeleteResponses)) {
|
||||
expect(response.status()).toBe(204);
|
||||
}
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Images updated successfully. It may take a few minutes to update.'
|
||||
);
|
||||
|
||||
// Without a custom logo the endpoint falls back to the logo bundled with Pocket ID
|
||||
await page.request
|
||||
.get('/api/application-images/logo?light=true')
|
||||
@@ -264,10 +368,8 @@ test.describe('Update application images', () => {
|
||||
const emailLogoInput = page.getByLabel('Email Logo', { exact: true });
|
||||
|
||||
await emailLogoInput.setInputFiles('resources/images/cloud-logo.svg');
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText(
|
||||
'File must be of type PNG or JPEG'
|
||||
);
|
||||
await expect(page.getByText('File must be of type PNG or JPEG', { exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,3 +20,24 @@ test('keyboard focus stays on sidebar link after navigating', async ({ page }) =
|
||||
await page.waitForURL('**/settings/audit-log');
|
||||
await expect(auditLog).toBeFocused();
|
||||
});
|
||||
|
||||
test('unsaved changes block navigation until they are discarded', async ({ page }) => {
|
||||
await page.goto('/settings/account');
|
||||
|
||||
const displayName = page.getByLabel('Display Name');
|
||||
const originalDisplayName = await displayName.inputValue();
|
||||
await displayName.fill('Pending navigation');
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toBeVisible();
|
||||
|
||||
const myApps = page.getByRole('link', { name: 'My Apps' });
|
||||
await myApps.click();
|
||||
await expect(page).toHaveURL(/\/settings\/account$/);
|
||||
|
||||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||||
await expect(displayName).toHaveValue(originalDisplayName);
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toHaveCount(0);
|
||||
|
||||
await myApps.click();
|
||||
await expect(page).toHaveURL(/\/settings\/apps$/);
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as jose from 'jose';
|
||||
import { oidcClients, userGroups } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import * as oidcUtil from '../utils/oidc.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
@@ -77,12 +78,7 @@ test('Edit OIDC client', async ({ page }) => {
|
||||
await page.locator('[role="tab"][data-value="dark-logo"]').first().click();
|
||||
await page.setInputFiles('#oidc-client-logo-dark', 'resources/images/cloud-logo.png');
|
||||
await page.getByLabel('Client Launch URL').fill(oidcClient.launchURL);
|
||||
const clientForm = page.getByLabel('Name').locator('xpath=ancestor::form');
|
||||
await clientForm.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'OIDC client updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
await expect(page.getByRole('img', { name: 'Nextcloud updated logo' }).first()).toBeVisible();
|
||||
await page.request
|
||||
.get(`/api/oidc/clients/${oidcClient.id}/logo`)
|
||||
@@ -134,8 +130,7 @@ test('Update OIDC client token lifetimes', async ({ page }) => {
|
||||
await expect(refreshLifetime).toHaveValue('720');
|
||||
await refreshLifetime.fill('336');
|
||||
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await expect(card.getByLabel('Access token lifetime', { exact: true })).toHaveValue('90');
|
||||
@@ -146,21 +141,44 @@ test('Update OIDC client token lifetimes', async ({ page }) => {
|
||||
await expect(card.getByLabel('Refresh token inactivity timeout unit')).toHaveText('Days');
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('0');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(card.getByText('Token lifetime must be at least 1 minute.')).toBeVisible();
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('525601');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(card.getByText('Token lifetime cannot exceed 365 days.')).toBeVisible();
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('1.5');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(card.getByText('Token lifetime must use whole-minute increments.')).toBeVisible();
|
||||
|
||||
await card.getByLabel('Access token lifetime', { exact: true }).fill('60');
|
||||
await card.getByLabel('Refresh token inactivity timeout', { exact: true }).fill('30');
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await saveUnsavedChanges(page);
|
||||
});
|
||||
|
||||
test('Save OIDC client details and token lifetimes together', async ({ page }) => {
|
||||
const client = oidcClients.nextcloud;
|
||||
await page.goto(`/settings/admin/oidc-clients/${client.id}`);
|
||||
|
||||
const name = page.getByLabel('Name');
|
||||
const accessLifetime = page
|
||||
.getByTestId('token-lifetimes-card')
|
||||
.getByLabel('Access token lifetime', { exact: true });
|
||||
await name.fill('Nextcloud with custom lifetime');
|
||||
await accessLifetime.fill('2');
|
||||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||||
await expect(name).toHaveValue(client.name);
|
||||
await expect(accessLifetime).toHaveValue('1');
|
||||
|
||||
await name.fill('Nextcloud with custom lifetime');
|
||||
await accessLifetime.fill('2');
|
||||
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await expect(name).toHaveValue('Nextcloud with custom lifetime');
|
||||
await expect(accessLifetime).toHaveValue('2');
|
||||
});
|
||||
|
||||
test('Update OIDC client federated credentials', async ({ page }) => {
|
||||
@@ -178,7 +196,7 @@ test('Update OIDC client federated credentials', async ({ page }) => {
|
||||
response.request().method() === 'PUT' &&
|
||||
response.url().endsWith(`/api/oidc/clients/${client.id}`)
|
||||
);
|
||||
await card.getByRole('button', { name: 'Save' }).click();
|
||||
await saveUnsavedChanges(page);
|
||||
expect((await cardUpdate).ok()).toBeTruthy();
|
||||
|
||||
await page.reload();
|
||||
@@ -186,17 +204,23 @@ test('Update OIDC client federated credentials', async ({ page }) => {
|
||||
await expect(card.getByLabel('Subject')).toHaveValue('workload-client');
|
||||
await expect(card.getByLabel('Audience')).toHaveValue('https://pocket-id.example.com');
|
||||
|
||||
await card.getByRole('radio', { name: 'Public keys' }).click();
|
||||
await card.getByRole('button', { name: 'Add another federated client credential' }).click();
|
||||
await expect(card.getByLabel('Issuer')).toHaveCount(2);
|
||||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||||
await expect(card.getByLabel('Issuer')).toHaveCount(1);
|
||||
await expect(card.getByRole('radio', { name: 'JWKS URL' })).toBeChecked();
|
||||
|
||||
// Saving the main client form must preserve credentials managed by the separate card
|
||||
await page.locator('[role="tab"][data-value="general"]').click();
|
||||
const description = page.getByLabel('Description');
|
||||
await description.fill('Updated without replacing federated credentials');
|
||||
const clientForm = description.locator('xpath=ancestor::form');
|
||||
const formUpdate = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === 'PUT' &&
|
||||
response.url().endsWith(`/api/oidc/clients/${client.id}`)
|
||||
);
|
||||
await clientForm.getByRole('button', { name: 'Save' }).click();
|
||||
await saveUnsavedChanges(page);
|
||||
expect((await formUpdate).ok()).toBeTruthy();
|
||||
|
||||
await page.goto(`/settings/admin/oidc-clients/${client.id}#credentials`);
|
||||
@@ -228,7 +252,7 @@ test('Update OIDC client federated credentials with public keys', async ({ page
|
||||
const pasteInput = card.getByLabel('Public key', { exact: true });
|
||||
const addKeyButton = card.getByRole('button', { name: 'Add public key' });
|
||||
const publicKeys = card.getByTestId('federated-identity-public-key');
|
||||
const saveButton = card.getByRole('button', { name: 'Save' });
|
||||
const saveButton = page.getByRole('button', { name: 'Save', exact: true });
|
||||
const waitForClientUpdate = () =>
|
||||
page.waitForResponse(
|
||||
(response) =>
|
||||
@@ -254,9 +278,7 @@ test('Update OIDC client federated credentials with public keys', async ({ page
|
||||
const privateKeyUpdate = waitForClientUpdate();
|
||||
await saveButton.click();
|
||||
expect((await privateKeyUpdate).status()).toBe(400);
|
||||
await expect(
|
||||
page.locator('[data-type="error"]').filter({ hasText: 'private key material' })
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/private key material/)).toBeVisible();
|
||||
await publicKeys.last().getByRole('button').click();
|
||||
await expect(publicKeys).toHaveCount(3);
|
||||
|
||||
@@ -268,9 +290,7 @@ test('Update OIDC client federated credentials with public keys', async ({ page
|
||||
const missingKeyIdUpdate = waitForClientUpdate();
|
||||
await saveButton.click();
|
||||
expect((await missingKeyIdUpdate).status()).toBe(400);
|
||||
await expect(
|
||||
page.locator('[data-type="error"]').filter({ hasText: 'missing the "kid" property' })
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/missing the "kid" property/)).toBeVisible();
|
||||
await publicKeys.last().getByRole('button').click();
|
||||
await expect(publicKeys).toHaveCount(3);
|
||||
|
||||
@@ -278,7 +298,7 @@ test('Update OIDC client federated credentials with public keys', async ({ page
|
||||
await expect(publicKeys).toHaveCount(2);
|
||||
|
||||
const cardUpdate = waitForClientUpdate();
|
||||
await saveButton.click();
|
||||
await saveUnsavedChanges(page);
|
||||
expect((await cardUpdate).ok()).toBeTruthy();
|
||||
|
||||
await page.reload();
|
||||
@@ -466,9 +486,7 @@ test('Update OIDC client allowed user groups', async ({ page }) => {
|
||||
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
||||
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.getByText('Allowed user groups updated successfully')).toBeVisible();
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import { generateIdToken } from '../utils/jwt.util';
|
||||
import * as oidcUtil from '../utils/oidc.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
@@ -1626,8 +1627,7 @@ test.describe('Pushed Authorization Requests (PAR)', () => {
|
||||
await parToggle.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).first().click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await saveUnsavedChanges(page);
|
||||
await page.reload();
|
||||
|
||||
await page.getByRole('button', { name: 'Show Advanced Options' }).click();
|
||||
@@ -1676,9 +1676,7 @@ test.describe('OIDC skip consent', () => {
|
||||
// Disabling it and saving must persist across a reload
|
||||
await toggle.click();
|
||||
await expect(toggle).not.toBeChecked();
|
||||
const clientForm = toggle.locator('xpath=ancestor::form');
|
||||
await clientForm.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await saveUnsavedChanges(page);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByRole('switch', { name: 'Skip Consent Screen' })).not.toBeChecked();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test, { expect, type Page } from '@playwright/test';
|
||||
import { cleanupBackend, cleanupScimServiceProvider } from 'utils/cleanup.util';
|
||||
import { oidcClients, userGroups, users } from '../data';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
async function configureOidcClient(page: Page) {
|
||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
||||
@@ -34,11 +35,17 @@ test.describe('SCIM Configuration', () => {
|
||||
|
||||
await page.getByLabel('SCIM Endpoint').fill('http://scim.provider/api');
|
||||
await page.getByLabel('SCIM Token').fill('supersecrettoken');
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toHaveCount(0);
|
||||
|
||||
await page.getByRole('button', { name: 'Enable' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('SCIM enabled successfully.');
|
||||
|
||||
await page.getByLabel('SCIM Endpoint').fill('http://edited.scim.provider/api');
|
||||
await expect(page.getByText('You have unsaved changes', { exact: true })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Discard', exact: true }).click();
|
||||
await expect(page.getByLabel('SCIM Endpoint')).toHaveValue('http://scim.provider/api');
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByLabel('SCIM Endpoint')).toHaveValue('http://scim.provider/api');
|
||||
@@ -54,11 +61,7 @@ test.describe('SCIM Configuration', () => {
|
||||
await page.getByLabel('SCIM Endpoint').fill('http://new.scim.provider/api');
|
||||
await page.getByLabel('SCIM Token').fill('evenmoresecrettoken');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'SCIM configuration updated successfully.'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -134,10 +137,7 @@ test.describe('SCIM Sync', () => {
|
||||
await developersCheckbox.click();
|
||||
await expect(developersCheckbox).toHaveAttribute('data-state', 'unchecked');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(
|
||||
page.getByText('Allowed user groups updated successfully', { exact: true })
|
||||
).toBeVisible();
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await syncScimServiceProvider(page);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { oidcClients, userGroups, users } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
@@ -34,9 +35,7 @@ test('Edit user group', async ({ page }) => {
|
||||
|
||||
await page.getByLabel('Name', { exact: true }).fill('developers_updated');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(0).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('User group updated successfully');
|
||||
await saveUnsavedChanges(page);
|
||||
await expect(page.getByLabel('Friendly Name')).toHaveValue('Developers updated');
|
||||
await expect(page.getByLabel('Name', { exact: true })).toHaveValue('developers_updated');
|
||||
});
|
||||
@@ -49,9 +48,7 @@ test('Update user group users', async ({ page }) => {
|
||||
await page.getByRole('row', { name: users.tim.username }).getByRole('checkbox').click();
|
||||
await page.getByRole('row', { name: users.craig.username }).getByRole('checkbox').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('Users updated successfully');
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -90,11 +87,7 @@ test('Update user group custom claims', async ({ page }) => {
|
||||
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
||||
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
@@ -107,11 +100,7 @@ test('Update user group custom claims', async ({ page }) => {
|
||||
|
||||
// Remove one custom claim
|
||||
await page.getByLabel('Remove custom claim').first().click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
@@ -136,11 +125,7 @@ test('Update user group allowed user groups', async ({ page }) => {
|
||||
await page.getByRole('row', { name: oidcClients.tailscale.name }).getByRole('checkbox').click();
|
||||
await page.getByRole('row', { name: oidcClients.immich.name }).getByRole('checkbox').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Allowed OIDC clients updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import test, { expect } from '@playwright/test';
|
||||
import { userGroups, users } from '../data';
|
||||
import authUtil from '../utils/auth.util';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
|
||||
@@ -128,9 +129,7 @@ test('Update user', async ({ page }) => {
|
||||
await page.getByLabel('Display Name').fill('Crack Apple');
|
||||
await page.getByLabel('Email').fill('crack.apple@test.com');
|
||||
await page.getByLabel('Username').fill('crack');
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('User updated successfully');
|
||||
await saveUnsavedChanges(page);
|
||||
});
|
||||
|
||||
test('Update user fails with already taken email', async ({ page }) => {
|
||||
@@ -145,9 +144,9 @@ test('Update user fails with already taken email', async ({ page }) => {
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('Email').fill(users.tim.email);
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Email is already in use');
|
||||
await expect(page.getByText('Email is already in use', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update user fails with already taken username', async ({ page }) => {
|
||||
@@ -162,9 +161,9 @@ test('Update user fails with already taken username', async ({ page }) => {
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('Username').fill(users.tim.username);
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||
await expect(page.getByText('Username is already in use', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update user fails with already taken username in different casing', async ({ page }) => {
|
||||
@@ -179,9 +178,9 @@ test('Update user fails with already taken username in different casing', async
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('Username').fill(users.tim.username.toUpperCase());
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||
await expect(page.getByText('Username is already in use', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Update user custom claims', async ({ page }) => {
|
||||
@@ -199,11 +198,7 @@ test('Update user custom claims', async ({ page }) => {
|
||||
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
||||
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -215,11 +210,7 @@ test('Update user custom claims', async ({ page }) => {
|
||||
|
||||
// Remove one custom claim
|
||||
await page.getByLabel('Remove custom claim').first().click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@@ -237,11 +228,7 @@ test('Update user group assignments', async ({ page }) => {
|
||||
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
||||
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'User groups updated successfully'
|
||||
);
|
||||
await saveUnsavedChanges(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import test, { expect, type Page } from '@playwright/test';
|
||||
import { signupTokens, userGroups, users } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
import { saveUnsavedChanges } from '../utils/unsaved-changes.util';
|
||||
|
||||
async function setSignupMode(
|
||||
page: Page,
|
||||
@@ -11,13 +12,12 @@ async function setSignupMode(
|
||||
await page.goto('/settings/admin/application-configuration');
|
||||
|
||||
await page.getByRole('tab', { name: 'User Creation' }).click();
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||
await page.getByRole('option', { name: mode }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
);
|
||||
const signupMode = page.getByRole('button', { name: 'Enable User Signups' });
|
||||
if (!(await signupMode.textContent())?.includes(mode)) {
|
||||
await signupMode.click();
|
||||
await page.getByRole('option', { name: mode }).click();
|
||||
await saveUnsavedChanges(page);
|
||||
}
|
||||
|
||||
if (signout) {
|
||||
await page.context().clearCookies();
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export async function saveUnsavedChanges(page: Page) {
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(page.getByText('Changes saved successfully', { exact: true })).toBeVisible();
|
||||
}
|
||||
Reference in New Issue
Block a user