mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added tests for custom integrations (#17770)
refs https://github.com/TryGhost/Product/issues/3729 --- This pull request enhances the custom integrations feature in the admin-x-settings app by adding accessibility and UI improvements to the `Select` and `WebhookModal` components, refactoring the webhook and integration types in the API module, and adding a new end-to-end test file to verify the functionality using mocked API responses.
This commit is contained in:
parent
df3aaed3c8
commit
49493abf75
5 changed files with 199 additions and 20 deletions
|
@ -17,6 +17,7 @@ export interface SelectOptionGroup {
|
|||
|
||||
export interface SelectProps {
|
||||
title?: string;
|
||||
hideTitle?: boolean;
|
||||
size?: 'xs' | 'md';
|
||||
prompt?: string;
|
||||
options: SelectOption[] | SelectOptionGroup[];
|
||||
|
@ -35,6 +36,7 @@ export interface SelectProps {
|
|||
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
title,
|
||||
hideTitle,
|
||||
size = 'md',
|
||||
prompt,
|
||||
options,
|
||||
|
@ -92,7 +94,7 @@ const Select: React.FC<SelectProps> = ({
|
|||
|
||||
const select = (
|
||||
<>
|
||||
{title && <Heading grey={selectedOption || !prompt ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={selectedOption || !prompt ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
<div className={containerClasses}>
|
||||
<select className={selectClasses} disabled={disabled} id={id} value={selectedOption} onChange={handleOptionChange}>
|
||||
{prompt && <option className={optionClasses} value="" disabled selected>{prompt}</option>}
|
||||
|
|
|
@ -1,34 +1,20 @@
|
|||
import {APIKey} from './apiKeys';
|
||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
||||
import {Webhook} from './webhooks';
|
||||
|
||||
// Types
|
||||
|
||||
export type IntegrationWebhook = {
|
||||
id: string;
|
||||
event: string;
|
||||
target_url: string;
|
||||
name: string;
|
||||
secret: string | null;
|
||||
api_version: string;
|
||||
integration_id: string;
|
||||
last_triggered_at: string | null;
|
||||
last_triggered_status: string | null;
|
||||
last_triggered_error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type Integration = {
|
||||
id: string;
|
||||
type: 'builtin' | 'core' | 'custom';
|
||||
name: string;
|
||||
slug: string;
|
||||
icon_image: string | null;
|
||||
description: string;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
api_keys?: APIKey[];
|
||||
webhooks?: IntegrationWebhook[];
|
||||
webhooks?: Webhook[];
|
||||
}
|
||||
|
||||
export interface IntegrationsResponseType {
|
||||
|
|
|
@ -126,7 +126,7 @@ const CustomIntegrationModal: React.FC<CustomIntegrationModalProps> = ({integrat
|
|||
onChange={e => updateForm(state => ({...state, name: e.target.value}))}
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextField title='Description' value={formState.description} onChange={e => updateForm(state => ({...state, description: e.target.value}))} />
|
||||
<TextField title='Description' value={formState.description || ''} onChange={e => updateForm(state => ({...state, description: e.target.value}))} />
|
||||
<div>
|
||||
<APIKeys keys={[
|
||||
{
|
||||
|
|
|
@ -56,7 +56,7 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId, onSa
|
|||
|
||||
return <Modal
|
||||
okColor='black'
|
||||
okLabel='Add'
|
||||
okLabel={webhook ? 'Update' : 'Add'}
|
||||
size='sm'
|
||||
testId='webhook-modal'
|
||||
title='Add webhook'
|
||||
|
@ -94,6 +94,8 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId, onSa
|
|||
options={webhookEventOptions}
|
||||
prompt='Select an event'
|
||||
selectedOption={formState.event}
|
||||
title='Event'
|
||||
hideTitle
|
||||
onSelect={(event) => {
|
||||
updateForm(state => ({...state, event}));
|
||||
clearError('event');
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
import {Integration, IntegrationsResponseType} from '../../../../src/api/integrations';
|
||||
import {Webhook, WebhooksResponseType} from '../../../../src/api/webhooks';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, mockApi} from '../../../utils/e2e';
|
||||
|
||||
test.describe('Custom integrations', async () => {
|
||||
test('Supports creating an integration and adding webhooks', async ({page}) => {
|
||||
const integration = {
|
||||
id: 'custom-id',
|
||||
type: 'custom',
|
||||
slug: 'my-integration',
|
||||
name: 'My integration',
|
||||
icon_image: null,
|
||||
description: null,
|
||||
created_at: '2023-01-01T00:00:00.000Z',
|
||||
updated_at: '2023-01-01T00:00:00.000Z',
|
||||
api_keys: [{
|
||||
id: 'admin-key-id',
|
||||
type: 'admin',
|
||||
secret: 'admin-api-secret',
|
||||
role_id: 'role-id',
|
||||
integration_id: 'my-integration',
|
||||
user_id: 'user-id',
|
||||
last_seen_at: null,
|
||||
last_seen_version: null,
|
||||
created_at: '2023-01-01T00:00:00.000Z',
|
||||
updated_at: '2023-01-01T00:00:00.000Z'
|
||||
}, {
|
||||
id: 'content-key-id',
|
||||
type: 'content',
|
||||
secret: 'content-api-secret',
|
||||
role_id: 'role-id',
|
||||
integration_id: 'my-integration',
|
||||
user_id: 'user-id',
|
||||
last_seen_at: null,
|
||||
last_seen_version: null,
|
||||
created_at: '2023-01-01T00:00:00.000Z',
|
||||
updated_at: '2023-01-01T00:00:00.000Z'
|
||||
}]
|
||||
} satisfies Integration;
|
||||
|
||||
const webhook = {
|
||||
id: 'webhook-id',
|
||||
event: 'post.created',
|
||||
target_url: 'https://example.com',
|
||||
name: 'My webhook',
|
||||
secret: null,
|
||||
api_version: 'v3',
|
||||
integration_id: 'my-integration',
|
||||
last_triggered_at: null,
|
||||
last_triggered_status: null,
|
||||
last_triggered_error: null,
|
||||
created_at: '2023-01-01T00:00:00.000Z',
|
||||
updated_at: '2023-01-01T00:00:00.000Z'
|
||||
} satisfies Webhook;
|
||||
|
||||
await mockApi({
|
||||
page,
|
||||
requests: {
|
||||
...globalDataRequests,
|
||||
browseIntegrations: {
|
||||
method: 'GET',
|
||||
path: '/integrations/?include=api_keys%2Cwebhooks',
|
||||
response: {integrations: []} satisfies IntegrationsResponseType
|
||||
},
|
||||
addIntegration: {
|
||||
method: 'POST',
|
||||
path: '/integrations/?include=api_keys%2Cwebhooks',
|
||||
response: {integrations: [integration]} satisfies IntegrationsResponseType
|
||||
},
|
||||
editIntegration: {
|
||||
method: 'PUT',
|
||||
path: `/integrations/${integration.id}/?include=api_keys%2Cwebhooks`,
|
||||
response: {integrations: [{
|
||||
...integration,
|
||||
description: 'Test description'
|
||||
}]} satisfies IntegrationsResponseType
|
||||
},
|
||||
deleteIntegration: {
|
||||
method: 'DELETE',
|
||||
path: `/integrations/${integration.id}/`,
|
||||
response: null
|
||||
},
|
||||
createWebhook: {
|
||||
method: 'POST',
|
||||
path: '/webhooks/',
|
||||
response: {webhooks: [webhook]} satisfies WebhooksResponseType
|
||||
},
|
||||
editWebhook: {
|
||||
method: 'PUT',
|
||||
path: `/webhooks/${webhook.id}/`,
|
||||
response: {webhooks: [{
|
||||
...webhook,
|
||||
name: 'Updated webhook'
|
||||
}]} satisfies WebhooksResponseType
|
||||
},
|
||||
deleteWebhook: {
|
||||
method: 'DELETE',
|
||||
path: `/webhooks/${webhook.id}/`,
|
||||
response: null
|
||||
},
|
||||
refreshAPIKey: {
|
||||
method: 'POST',
|
||||
path: /\/api_key\/.+\/refresh/,
|
||||
response: ({
|
||||
integrations: [{
|
||||
...integration,
|
||||
api_keys: [{
|
||||
...integration.api_keys[0],
|
||||
secret: 'new-api-key'
|
||||
}]
|
||||
}]
|
||||
} satisfies IntegrationsResponseType)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const integrationsSection = page.getByTestId('integrations');
|
||||
|
||||
await integrationsSection.getByRole('button', {name: 'Add custom integration'}).click();
|
||||
|
||||
const createModal = page.getByTestId('add-integration-modal');
|
||||
|
||||
createModal.getByLabel('Name').fill('My integration');
|
||||
createModal.getByRole('button', {name: 'Add'}).click();
|
||||
|
||||
const modal = page.getByTestId('custom-integration-modal');
|
||||
|
||||
// Regenerate API key
|
||||
|
||||
await expect(modal).toHaveText(/admin-api-secret/);
|
||||
await modal.getByText('admin-api-secret').hover();
|
||||
await modal.getByRole('button', {name: 'Regenerate'}).click();
|
||||
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Regenerate Admin API Key'}).click();
|
||||
|
||||
await expect(modal).toHaveText(/Admin API Key was successfully regenerated/);
|
||||
await expect(modal).toHaveText(/new-api-key/);
|
||||
|
||||
// Create webhook
|
||||
|
||||
await modal.getByRole('button', {name: 'Add webhook'}).click();
|
||||
|
||||
const webhookModal = page.getByTestId('webhook-modal');
|
||||
|
||||
await webhookModal.getByLabel('Name').fill('My webhook');
|
||||
await webhookModal.getByLabel('Target URL').fill('https://example.com');
|
||||
await webhookModal.getByLabel('Event').selectOption('Post created');
|
||||
|
||||
// Playwright fails unless you click twice, for some reason (timing issue with validations?)
|
||||
await webhookModal.getByRole('button', {name: 'Add'}).click();
|
||||
await webhookModal.getByRole('button', {name: 'Add'}).click();
|
||||
|
||||
await expect(modal).toHaveText(/My webhook/);
|
||||
|
||||
// Edit webhook
|
||||
|
||||
await modal.getByText('My webhook').click();
|
||||
|
||||
await webhookModal.getByLabel('Name').fill('Updated webhook');
|
||||
await webhookModal.getByRole('button', {name: 'Update'}).click();
|
||||
|
||||
await expect(modal).toHaveText(/Updated webhook/);
|
||||
|
||||
// Delete webhook
|
||||
|
||||
await modal.getByText('Updated webhook').hover();
|
||||
await modal.getByRole('button', {name: 'Delete'}).click();
|
||||
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Delete webhook'}).click();
|
||||
|
||||
await expect(modal).not.toHaveText(/Updated webhook/);
|
||||
|
||||
// Edit integration
|
||||
|
||||
await modal.getByLabel('Description').fill('Test description');
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(integrationsSection).toHaveText(/Test description/);
|
||||
|
||||
// Delete integration
|
||||
|
||||
await integrationsSection.getByText('My integration').hover();
|
||||
await integrationsSection.getByRole('button', {name: 'Delete'}).click();
|
||||
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Delete integration'}).click();
|
||||
|
||||
await expect(integrationsSection).not.toHaveText(/My integration/);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue