Ghost/apps/admin-x-settings/test/acceptance/general/users/profile.test.ts

400 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 & close'}).click();
await expect(modal).toContainText('Name is required');
await modal.getByLabel('Full name').fill(new Array(195).join('a'));
await modal.getByRole('button', {name: 'Save & close'}).click();
await expect(modal).toContainText('Name is too long');
await modal.getByLabel('Email').fill('test');
await modal.getByRole('button', {name: 'Save & close'}).click();
await expect(modal).toContainText('Please enter a valid email address');
await modal.getByLabel('Location').fill(new Array(195).join('a'));
await modal.getByRole('button', {name: 'Save & close'}).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 & close'}).click();
await expect(modal).toContainText('Bio is too long');
await modal.getByLabel('Website').fill('not-a-website');
await modal.getByLabel('Website').blur();
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,
'twitter.com/username',
'https://twitter.com/username'
);
await testUrlValidation(
twitterInput,
'testuser',
'https://twitter.com/testuser'
);
await testUrlValidation(
twitterInput,
'http://github.com/username',
'https://twitter.com/username'
);
await testUrlValidation(
twitterInput,
'*(&*(%%))',
'*(&*(%%))',
'The URL must be in a format like https://twitter.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 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 & close'}).click();
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
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: 'Cancel'}).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: 'Cancel'}).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();
});
});