mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
6378d7d66f
DES-27 There are two patterns used in settings modals for action buttons: 1. [Cancel] and [Save & close] (sometimes it's [Cancel] and [OK], inconsistently) — example: Staff details, Tier details, Navigation, Recommendation 2. [Close] and [Save] — example: Design settings, Portal, Newsletter details etc. This is confusing and leaves people confused and uncertain about what's going to happen in one or the other case.
397 lines
15 KiB
TypeScript
397 lines
15 KiB
TypeScript
import {StaffTokenResponseType} from '@tryghost/admin-x-framework/api/staffToken';
|
|
import {expect, test} from '@playwright/test';
|
|
import {globalDataRequests} from '../../../utils/acceptance';
|
|
import {mockApi, responseFixtures, testUrlValidation} from '@tryghost/admin-x-framework/test/acceptance';
|
|
|
|
test.describe('User profile', async () => {
|
|
test('Supports editing user profiles', async ({page}) => {
|
|
const userToEdit = responseFixtures.users.users.find(user => user.email === 'administrator@test.com')!;
|
|
|
|
const {lastApiRequests} = await mockApi({page, requests: {
|
|
...globalDataRequests,
|
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
|
users: [{
|
|
...userToEdit,
|
|
email: 'newadmin@test.com',
|
|
name: 'New Admin'
|
|
}]
|
|
}}
|
|
}});
|
|
|
|
await page.goto('/');
|
|
|
|
const section = page.getByTestId('users');
|
|
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
|
|
|
await section.getByRole('tab', {name: 'Administrators'}).click();
|
|
|
|
const listItem = activeTab.getByTestId('user-list-item').last();
|
|
await listItem.hover();
|
|
await listItem.getByRole('button', {name: 'Edit'}).click();
|
|
|
|
const modal = page.getByTestId('user-detail-modal');
|
|
|
|
// Validation failures
|
|
|
|
await modal.getByLabel('Full name').fill('');
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
await expect(modal).toContainText('Name is required');
|
|
|
|
await modal.getByLabel('Email').fill('test');
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
await expect(modal).toContainText('Enter a valid email address');
|
|
|
|
await modal.getByLabel('Location').fill(new Array(195).join('a'));
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
await expect(modal).toContainText('Location is too long');
|
|
|
|
await modal.getByLabel('Bio').fill(new Array(210).join('a'));
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
await expect(modal).toContainText('Bio is too long');
|
|
|
|
await modal.getByLabel('Website').fill('not-a-website');
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
await expect(modal).toContainText('Enter a valid URL');
|
|
|
|
const facebookInput = modal.getByLabel('Facebook profile');
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'facebook.com/username',
|
|
'https://www.facebook.com/username'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'testuser',
|
|
'https://www.facebook.com/testuser'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'ab99',
|
|
'https://www.facebook.com/ab99'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'page/ab99',
|
|
'https://www.facebook.com/page/ab99'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'page/*(&*(%%))',
|
|
'https://www.facebook.com/page/*(&*(%%))'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'facebook.com/pages/some-facebook-page/857469375913?ref=ts',
|
|
'https://www.facebook.com/pages/some-facebook-page/857469375913?ref=ts'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'https://www.facebook.com/groups/savethecrowninn',
|
|
'https://www.facebook.com/groups/savethecrowninn'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'http://github.com/username',
|
|
'http://github.com/username',
|
|
'The URL must be in a format like https://www.facebook.com/yourPage'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
facebookInput,
|
|
'http://github.com/pages/username',
|
|
'http://github.com/pages/username',
|
|
'The URL must be in a format like https://www.facebook.com/yourPage'
|
|
);
|
|
|
|
const twitterInput = modal.getByLabel('X (formerly Twitter) profile');
|
|
|
|
await testUrlValidation(
|
|
twitterInput,
|
|
'x.com/username',
|
|
'https://x.com/username'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
twitterInput,
|
|
'testuser',
|
|
'https://x.com/testuser'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
twitterInput,
|
|
'http://github.com/username',
|
|
'https://x.com/username'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
twitterInput,
|
|
'*(&*(%%))',
|
|
'*(&*(%%))',
|
|
'The URL must be in a format like https://x.com/yourUsername'
|
|
);
|
|
|
|
await testUrlValidation(
|
|
twitterInput,
|
|
'thisusernamehasmorethan15characters',
|
|
'thisusernamehasmorethan15characters',
|
|
'Your Username is not a valid Twitter Username'
|
|
);
|
|
|
|
// Successful update
|
|
|
|
await modal.getByLabel('Full name').fill('New Admin');
|
|
await modal.getByLabel('Email').fill('newadmin@test.com');
|
|
await modal.getByLabel('Slug').fill('newadmin');
|
|
await expect(modal.getByText('https://example.com/author/newadmin')).toBeVisible();
|
|
await modal.getByLabel('Location').fill('some location');
|
|
await modal.getByLabel('Website').fill('https://example.com');
|
|
await modal.getByLabel('Facebook profile').fill('fb');
|
|
await modal.getByLabel('X (formerly Twitter) profile').fill('tw');
|
|
await modal.getByLabel('Bio').fill('some bio');
|
|
|
|
// Email notification settings
|
|
|
|
await modal.getByLabel(/Comments/).uncheck();
|
|
await modal.getByLabel(/New signups/).uncheck();
|
|
await modal.getByLabel(/New paid members/).uncheck();
|
|
await modal.getByLabel(/Paid member cancellations/).check();
|
|
await modal.getByLabel(/Milestones/).uncheck();
|
|
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
|
|
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
|
await modal.getByRole('button', {name: 'Close'}).click();
|
|
|
|
await expect(listItem.getByText('New Admin')).toBeVisible();
|
|
await expect(listItem.getByText('newadmin@test.com')).toBeVisible();
|
|
|
|
expect(lastApiRequests.editUser?.body).toMatchObject({
|
|
users: [{
|
|
email: 'newadmin@test.com',
|
|
name: 'New Admin',
|
|
slug: 'newadmin',
|
|
location: 'some location',
|
|
website: 'https://example.com',
|
|
facebook: 'fb',
|
|
twitter: '@tw',
|
|
bio: 'some bio',
|
|
comment_notifications: false,
|
|
free_member_signup_notification: false,
|
|
paid_subscription_started_notification: false,
|
|
paid_subscription_canceled_notification: true,
|
|
milestone_notifications: false
|
|
}]
|
|
});
|
|
});
|
|
|
|
test('Supports uploading profile picture', async ({page}) => {
|
|
const userToEdit = responseFixtures.users.users.find(user => user.email === 'owner@test.com')!;
|
|
|
|
const {lastApiRequests} = await mockApi({page, requests: {
|
|
...globalDataRequests,
|
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
|
uploadImage: {method: 'POST', path: '/images/upload/', response: {images: [{url: 'http://example.com/image.png', ref: null}]}},
|
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
|
users: [{
|
|
...userToEdit,
|
|
profile_image: 'http://example.com/image.png',
|
|
cover_image: 'http://example.com/image.png'
|
|
}]
|
|
}}
|
|
}});
|
|
|
|
await page.goto('/');
|
|
|
|
const section = page.getByTestId('users');
|
|
|
|
const wrapper = section.getByTestId('owner-user');
|
|
await wrapper.hover();
|
|
await wrapper.getByRole('button', {name: 'View profile'}).click();
|
|
|
|
// Upload profile picture
|
|
|
|
const modal = page.getByTestId('user-detail-modal');
|
|
|
|
const profileFileChooserPromise = page.waitForEvent('filechooser');
|
|
|
|
await modal.locator('label[for=avatar]').click();
|
|
|
|
const profileFileChooser = await profileFileChooserPromise;
|
|
await profileFileChooser.setFiles(`${__dirname}/../../../utils/images/image.png`);
|
|
|
|
await expect(modal.locator('#avatar')).toHaveAttribute('src', 'http://example.com/image.png');
|
|
|
|
// Upload cover image
|
|
|
|
const coverFileChooserPromise = page.waitForEvent('filechooser');
|
|
|
|
await modal.locator('label[for=cover-image]').click();
|
|
|
|
const coverFileChooser = await coverFileChooserPromise;
|
|
await coverFileChooser.setFiles(`${__dirname}/../../../utils/images/image.png`);
|
|
|
|
await expect(modal.locator('#cover-image')).toHaveAttribute('src', 'http://example.com/image.png');
|
|
|
|
// Save the user
|
|
|
|
await modal.getByRole('button', {name: 'Save'}).click();
|
|
|
|
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
|
|
|
expect(lastApiRequests.editUser?.body).toMatchObject({
|
|
users: [{
|
|
email: 'owner@test.com',
|
|
profile_image: 'http://example.com/image.png',
|
|
cover_image: 'http://example.com/image.png'
|
|
}]
|
|
});
|
|
});
|
|
|
|
test('Does not show email notification options for non-admin users', async ({page}) => {
|
|
const userToEdit = responseFixtures.users.users.find(user => user.email === 'editor@test.com')!;
|
|
|
|
await mockApi({page, requests: {
|
|
...globalDataRequests,
|
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
|
users: [{
|
|
...userToEdit,
|
|
email: 'newadmin@test.com',
|
|
name: 'New Admin'
|
|
}]
|
|
}}
|
|
}});
|
|
|
|
await page.goto('/');
|
|
|
|
const section = page.getByTestId('users');
|
|
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
|
|
|
await section.getByRole('tab', {name: 'Editors'}).click();
|
|
|
|
const listItem = activeTab.getByTestId('user-list-item').last();
|
|
await listItem.hover();
|
|
await listItem.getByRole('button', {name: 'Edit'}).click();
|
|
|
|
const modal = page.getByTestId('user-detail-modal');
|
|
|
|
await expect(modal.getByLabel(/Comments/)).toBeVisible();
|
|
await expect(modal.getByLabel(/New signups/)).toBeHidden();
|
|
await expect(modal.getByLabel(/New paid members/)).toBeHidden();
|
|
await expect(modal.getByLabel(/Paid member cancellations/)).toBeHidden();
|
|
await expect(modal.getByLabel(/Milestones/)).toBeHidden();
|
|
});
|
|
|
|
test('Warns when leaving without saving', async ({page}) => {
|
|
const userToEdit = responseFixtures.users.users.find(user => user.email === 'administrator@test.com')!;
|
|
|
|
const {lastApiRequests} = await mockApi({page, requests: {
|
|
...globalDataRequests,
|
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: responseFixtures.users}
|
|
}});
|
|
|
|
await page.goto('/');
|
|
|
|
const section = page.getByTestId('users');
|
|
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
|
|
|
await section.getByRole('tab', {name: 'Administrators'}).click();
|
|
|
|
const listItem = activeTab.getByTestId('user-list-item').last();
|
|
await listItem.hover();
|
|
await listItem.getByRole('button', {name: 'Edit'}).click();
|
|
|
|
const modal = page.getByTestId('user-detail-modal');
|
|
|
|
await modal.getByLabel('Full name').fill('Updated');
|
|
|
|
await modal.getByRole('button', {name: 'Close'}).click();
|
|
|
|
await expect(page.getByTestId('confirmation-modal')).toHaveText(/leave/i);
|
|
|
|
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Leave'}).click();
|
|
|
|
await expect(modal).toBeHidden();
|
|
expect(lastApiRequests.editUser).toBeUndefined();
|
|
});
|
|
|
|
test('Supports managing staff token', async ({page}) => {
|
|
const userToEdit = responseFixtures.users.users.find(user => user.email === 'owner@test.com')!;
|
|
|
|
const apiKey = {
|
|
id: 'token-id',
|
|
created_at: '2023-01-01',
|
|
integration_id: 'integration-id',
|
|
last_seen_at: null,
|
|
last_seen_version: null,
|
|
role_id: 'role-id',
|
|
secret: 'secret',
|
|
type: '',
|
|
updated_at: '2023-01-01',
|
|
user_id: userToEdit.id
|
|
};
|
|
|
|
const {lastApiRequests} = await mockApi({page, requests: {
|
|
...globalDataRequests,
|
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
|
|
|
getStaffToken: {method: 'GET', path: '/users/me/token/', response: {apiKey} satisfies StaffTokenResponseType},
|
|
|
|
genStaffToken: {
|
|
method: 'PUT',
|
|
path: '/users/me/token/',
|
|
response: {
|
|
apiKey: {
|
|
...apiKey,
|
|
secret: 'new-secret'
|
|
}
|
|
} satisfies StaffTokenResponseType
|
|
}
|
|
}});
|
|
|
|
await page.goto('/');
|
|
|
|
const section = page.getByTestId('users');
|
|
const modal = page.getByTestId('user-detail-modal');
|
|
|
|
// Can't see the staff token for other users
|
|
|
|
const activeTab = section.locator('[role=tabpanel]:not(.hidden)');
|
|
|
|
await section.getByRole('tab', {name: 'Administrators'}).click();
|
|
|
|
const listItem = activeTab.getByTestId('user-list-item').last();
|
|
await listItem.hover();
|
|
await listItem.getByRole('button', {name: 'Edit'}).click();
|
|
|
|
await expect(modal.getByTestId('api-keys')).toBeHidden();
|
|
await modal.getByRole('button', {name: 'Close'}).click();
|
|
|
|
// Can see and regenerate your own staff token
|
|
|
|
const ownerItem = section.getByTestId('owner-user').last();
|
|
await ownerItem.hover();
|
|
await ownerItem.getByRole('button', {name: 'View profile'}).click();
|
|
|
|
await expect(modal.getByTestId('api-keys')).toContainText('token-id:secret');
|
|
|
|
await modal.getByTestId('api-keys').hover();
|
|
await modal.getByTestId('api-keys').getByRole('button', {name: 'Regenerate'}).click();
|
|
|
|
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Regenerate your Staff Access Token'}).click();
|
|
|
|
await expect(modal.getByTestId('api-keys')).toContainText('token-id:new-secret');
|
|
|
|
expect(lastApiRequests.genStaffToken).toBeTruthy();
|
|
});
|
|
});
|