0
Fork 0
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:
Jono M 2023-08-22 11:25:25 +01:00 committed by GitHub
parent df3aaed3c8
commit 49493abf75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 20 deletions

View file

@ -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>}

View file

@ -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 {

View file

@ -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={[
{

View file

@ -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');

View file

@ -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/);
});
});