62a5b7d61a
Fixes [PROD-266](https://linear.app/tryghost/issue/PROD-266/remove-onblur-error-feedback-in-recommendation-edit-modal)
400 lines
15 KiB
TypeScript
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();
|
|
});
|
|
});
|