MM-68022 Bulk migration H18-28 Enzyme to RTL (#35753)

This commit is contained in:
sabril
2026-03-27 15:06:12 +08:00
committed by GitHub
parent a6d1942ff6
commit adbe736576
72 changed files with 14763 additions and 12413 deletions
@@ -1,420 +1,493 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/DatabaseSettings should match snapshot 1`] = `
<form
className="form-horizontal"
onSubmit={[Function]}
role="form"
>
<div
className="wrapper--fixed"
<div>
<form
class="form-horizontal"
role="form"
>
<AdminHeader>
<MemoizedFormattedMessage
defaultMessage="Database"
id="admin.database.title"
/>
</AdminHeader>
<Memo(SettingsGroup)>
<div
class="wrapper--fixed"
>
<div
className="banner"
class="admin-console__header"
>
<MemoizedFormattedMessage
defaultMessage="Changing properties in this section will require a server restart before taking effect."
id="admin.sql.noteDescription"
/>
Database
</div>
<div
className="form-group"
class="admin-console__wrapper"
>
<label
className="control-label col-sm-4"
htmlFor="DriverName"
>
<MemoizedFormattedMessage
defaultMessage="Driver Name:"
id="admin.sql.driverName"
/>
</label>
<div
className="col-sm-8"
class="admin-console__content"
>
<input
className="form-control"
disabled={true}
type="text"
/>
<div
className="help-text"
class="banner"
>
<MemoizedFormattedMessage
defaultMessage="Set the database driver in the config.json file."
id="admin.sql.driverNameDescription"
/>
Changing properties in this section will require a server restart before taking effect.
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="DataSource"
>
<MemoizedFormattedMessage
defaultMessage="Data Source:"
id="admin.sql.dataSource"
/>
</label>
<div
className="col-sm-8"
>
<input
className="form-control"
disabled={true}
type="text"
value="**********@localhost/mattermost_test?sslmode=disable&connect_timeout=10"
/>
<div
className="help-text"
class="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Set the database source in the config.json file."
id="admin.sql.dataSourceDescription"
/>
<label
class="control-label col-sm-4"
for="DriverName"
>
Driver Name:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
disabled=""
type="text"
value=""
/>
<div
class="help-text"
>
Set the database driver in the config.json file.
</div>
</div>
</div>
</div>
</div>
<AdminTextSetting
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum number of idle connections held open to the database."
id="admin.sql.maxConnectionsDescription"
/>
}
id="maxIdleConns"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum Idle Connections:"
id="admin.sql.maxConnectionsTitle"
/>
}
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"10\\"",
"id": "admin.sql.maxConnectionsExample",
}
}
setByEnv={false}
type="text"
value={10}
/>
<AdminTextSetting
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum number of open connections held open to the database."
id="admin.sql.maxOpenDescription"
/>
}
id="maxOpenConns"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum Open Connections:"
id="admin.sql.maxOpenTitle"
/>
}
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"10\\"",
"id": "admin.sql.maxOpenExample",
}
}
setByEnv={false}
type="text"
value={100}
/>
<AdminTextSetting
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="The number of seconds to wait for a response from the database after opening a connection and sending the query. Errors that you see in the UI or in the logs as a result of a query timeout can vary depending on the type of query."
id="admin.sql.queryTimeoutDescription"
/>
}
id="queryTimeout"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Query Timeout:"
id="admin.sql.queryTimeoutTitle"
/>
}
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"30\\"",
"id": "admin.sql.queryTimeoutExample",
}
}
setByEnv={false}
type="text"
value={10}
/>
<AdminTextSetting
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum lifetime for a connection to the database in milliseconds."
id="admin.sql.connMaxLifetimeDescription"
/>
}
id="connMaxLifetimeMilliseconds"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum Connection Lifetime:"
id="admin.sql.connMaxLifetimeTitle"
/>
}
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"3600000\\"",
"id": "admin.sql.connMaxLifetimeExample",
}
}
setByEnv={false}
type="text"
value={10}
/>
<AdminTextSetting
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum idle time for a connection to the database in milliseconds."
id="admin.sql.connMaxIdleTimeDescription"
/>
}
id="connMaxIdleTimeMilliseconds"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Maximum Connection Idle Time:"
id="admin.sql.connMaxIdleTimeTitle"
/>
}
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"300000\\"",
"id": "admin.sql.connMaxIdleTimeExample",
}
}
setByEnv={false}
type="text"
value={20}
/>
<AdminTextSetting
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Minimum number of characters in a hashtag. This must be greater than or equal to 2."
id="admin.service.minimumHashtagLengthDescription"
/>
}
id="minimumHashtagLength"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Minimum Hashtag Length:"
id="admin.service.minimumHashtagLengthTitle"
/>
}
onChange={[Function]}
placeholder={
Object {
"defaultMessage": "E.g.: \\"3\\"",
"id": "admin.service.minimumHashtagLengthExample",
}
}
setByEnv={false}
type="text"
value={10}
/>
<Memo(BooleanSetting)
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="(Development Mode) When true, executing SQL statements are written to the log."
id="admin.sql.traceDescription"
/>
}
id="trace"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="SQL Statement Logging: "
id="admin.sql.traceTitle"
/>
}
onChange={[Function]}
setByEnv={false}
value={false}
/>
<RequestButton
buttonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Recycle Database Connections"
id="admin.recycle.button"
/>
}
disabled={false}
errorMessage={
Object {
"defaultMessage": "Recycling unsuccessful: {error}",
"id": "admin.recycle.reloadFail",
}
}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating \\"config.json\\" to the new desired configuration and using the {reloadConfiguration} feature to load the new settings while the server is running. The administrator should then use {featureName} feature to recycle the database connections based on the new settings."
id="admin.recycle.recycleDescription"
values={
Object {
"featureName": <b>
<Memo(MemoizedFormattedMessage)
defaultMessage="Recycle Database Connections"
id="admin.recycle.recycleDescription.featureName"
/>
</b>,
"reloadConfiguration": <a
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="DataSource"
>
Data Source:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
disabled=""
type="text"
value="**********@localhost/mattermost_test?sslmode=disable&connect_timeout=10"
/>
<div
class="help-text"
>
Set the database source in the config.json file.
</div>
</div>
</div>
<div
class="form-group"
data-testid="maxIdleConns"
>
<label
class="control-label col-sm-4"
data-testid="maxIdleConnslabel"
for="maxIdleConns"
>
Maximum Idle Connections:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
data-testid="maxIdleConnsinput"
id="maxIdleConns"
maxlength="-1"
placeholder="E.g.: \\"10\\""
type="text"
value="10"
/>
<div
class="help-text"
data-testid="maxIdleConnshelp-text"
>
Maximum number of idle connections held open to the database.
</div>
</div>
</div>
<div
class="form-group"
data-testid="maxOpenConns"
>
<label
class="control-label col-sm-4"
data-testid="maxOpenConnslabel"
for="maxOpenConns"
>
Maximum Open Connections:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
data-testid="maxOpenConnsinput"
id="maxOpenConns"
maxlength="-1"
placeholder="E.g.: \\"10\\""
type="text"
value="100"
/>
<div
class="help-text"
data-testid="maxOpenConnshelp-text"
>
Maximum number of open connections held open to the database.
</div>
</div>
</div>
<div
class="form-group"
data-testid="queryTimeout"
>
<label
class="control-label col-sm-4"
data-testid="queryTimeoutlabel"
for="queryTimeout"
>
Query Timeout:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
data-testid="queryTimeoutinput"
id="queryTimeout"
maxlength="-1"
placeholder="E.g.: \\"30\\""
type="text"
value="10"
/>
<div
class="help-text"
data-testid="queryTimeouthelp-text"
>
The number of seconds to wait for a response from the database after opening a connection and sending the query. Errors that you see in the UI or in the logs as a result of a query timeout can vary depending on the type of query.
</div>
</div>
</div>
<div
class="form-group"
data-testid="connMaxLifetimeMilliseconds"
>
<label
class="control-label col-sm-4"
data-testid="connMaxLifetimeMillisecondslabel"
for="connMaxLifetimeMilliseconds"
>
Maximum Connection Lifetime:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
data-testid="connMaxLifetimeMillisecondsinput"
id="connMaxLifetimeMilliseconds"
maxlength="-1"
placeholder="E.g.: \\"3600000\\""
type="text"
value="10"
/>
<div
class="help-text"
data-testid="connMaxLifetimeMillisecondshelp-text"
>
Maximum lifetime for a connection to the database in milliseconds.
</div>
</div>
</div>
<div
class="form-group"
data-testid="connMaxIdleTimeMilliseconds"
>
<label
class="control-label col-sm-4"
data-testid="connMaxIdleTimeMillisecondslabel"
for="connMaxIdleTimeMilliseconds"
>
Maximum Connection Idle Time:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
data-testid="connMaxIdleTimeMillisecondsinput"
id="connMaxIdleTimeMilliseconds"
maxlength="-1"
placeholder="E.g.: \\"300000\\""
type="text"
value="20"
/>
<div
class="help-text"
data-testid="connMaxIdleTimeMillisecondshelp-text"
>
Maximum idle time for a connection to the database in milliseconds.
</div>
</div>
</div>
<div
class="form-group"
data-testid="minimumHashtagLength"
>
<label
class="control-label col-sm-4"
data-testid="minimumHashtagLengthlabel"
for="minimumHashtagLength"
>
Minimum Hashtag Length:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
data-testid="minimumHashtagLengthinput"
id="minimumHashtagLength"
maxlength="-1"
placeholder="E.g.: \\"3\\""
type="text"
value="10"
/>
<div
class="help-text"
data-testid="minimumHashtagLengthhelp-text"
>
Minimum number of characters in a hashtag. This must be greater than or equal to 2.
</div>
</div>
</div>
<fieldset
class="form-group"
data-testid="trace"
id="trace"
>
<legend
class="control-label form-legend col-sm-4"
>
SQL Statement Logging:
</legend>
<div
class="col-sm-8"
>
<label
class="Label-eJcoMl iShgPr"
>
<input
data-testid="tracetrue"
id="tracetrue"
name="trace"
type="radio"
value="true"
/>
True
</label>
<label
class="Label-eJcoMl iShgPr"
>
<input
checked=""
data-testid="tracefalse"
id="tracefalse"
name="trace"
type="radio"
value="false"
/>
False
</label>
<div
class="help-text"
data-testid="tracehelp-text"
>
(Development Mode) When true, executing SQL statements are written to the log.
</div>
</div>
</fieldset>
<div
class="form-group"
>
<div
class="col-sm-offset-4 col-sm-8"
>
<div>
<button
class="btn btn-tertiary"
type="button"
>
Recycle Database Connections
</button>
</div>
<div
class="help-text"
>
Deployments using multiple databases can switch from one master database to another without restarting the Mattermost server by updating "config.json" to the new desired configuration and using the
<a
href="../environment/web_server"
>
<b>
<Memo(MemoizedFormattedMessage)
defaultMessage="Environment > Web Server > Reload Configuration from Disk"
id="admin.recycle.recycleDescription.reloadConfiguration"
/>
Environment &gt; Web Server &gt; Reload Configuration from Disk
</b>
</a>,
}
}
/>
}
includeDetailedError={true}
requestAction={[MockFunction]}
saveNeeded={false}
showSuccessMessage={false}
successMessage={
Object {
"defaultMessage": "Test Successful",
"id": "admin.requestButton.requestSuccess",
}
}
/>
<Memo(BooleanSetting)
disabled={false}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Disables the use of the database to perform searches. Should only be used when other <link>search engines</link> are configured."
id="admin.sql.disableDatabaseSearchDescription"
values={
Object {
"link": [Function],
}
}
/>
}
id="disableDatabaseSearch"
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Disable database search: "
id="admin.sql.disableDatabaseSearchTitle"
/>
}
onChange={[Function]}
setByEnv={false}
value={true}
/>
<SettingSet
label={
<Memo(MemoizedFormattedMessage)
defaultMessage="Schema Migrations:"
id="admin.database.migrations_table.title"
/>
}
>
<div
className="migrations-table-setting"
>
<Connect(Component)
createHelpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="All applied migrations."
id="admin.database.migrations_table.help_text"
/>
}
/>
</div>
</SettingSet>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="activeSearchBackend"
>
<MemoizedFormattedMessage
defaultMessage="Active Search Backend:"
id="admin.database.search_backend.title"
/>
</label>
<div
className="col-sm-8"
>
<input
className="form-control"
disabled={true}
id="activeSearchBackend"
type="text"
value=""
/>
<div
className="help-text"
</a>
feature to load the new settings while the server is running. The administrator should then use
<b>
Recycle Database Connections
</b>
feature to recycle the database connections based on the new settings.
</div>
</div>
</div>
<fieldset
class="form-group"
data-testid="disableDatabaseSearch"
id="disableDatabaseSearch"
>
<MemoizedFormattedMessage
defaultMessage="Shows the currently active backend used for search. Values can be none, database, elasticsearch."
id="admin.database.search_backend.help_text"
/>
<legend
class="control-label form-legend col-sm-4"
>
Disable database search:
</legend>
<div
class="col-sm-8"
>
<label
class="Label-eJcoMl iShgPr"
>
<input
checked=""
data-testid="disableDatabaseSearchtrue"
id="disableDatabaseSearchtrue"
name="disableDatabaseSearch"
type="radio"
value="true"
/>
True
</label>
<label
class="Label-eJcoMl iShgPr"
>
<input
data-testid="disableDatabaseSearchfalse"
id="disableDatabaseSearchfalse"
name="disableDatabaseSearch"
type="radio"
value="false"
/>
False
</label>
<div
class="help-text"
data-testid="disableDatabaseSearchhelp-text"
>
Disables the use of the database to perform searches. Should only be used when other
<a
href="https://mattermost.com/pl/setup-elasticsearch?utm_source=mattermost&utm_medium=in-product&utm_content=database_settings&uid=&sid=&edition=team&server_version="
location="database_settings"
rel="noopener noreferrer"
target="_blank"
>
search engines
</a>
are configured.
</div>
</div>
</fieldset>
<fieldset
class="form-group"
>
<legend
class="control-label form-legend col-sm-4"
>
Schema Migrations:
</legend>
<div
class="col-sm-8"
>
<div
class="migrations-table-setting"
>
<div
class="MigrationsTable migrations-table__panel"
>
<div
class="help-text"
>
All applied migrations.
</div>
<div
class="migrations-table__table"
>
<table
class="table"
data-testid="migrationsTable"
>
<thead>
<tr>
<th>
Version
</th>
<th>
Name
</th>
</tr>
</thead>
<tbody />
</table>
</div>
</div>
</div>
</div>
</fieldset>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="activeSearchBackend"
>
Active Search Backend:
</label>
<div
class="col-sm-8"
>
<input
class="form-control"
disabled=""
id="activeSearchBackend"
type="text"
value=""
/>
<div
class="help-text"
>
Shows the currently active backend used for search. Values can be none, database, elasticsearch.
</div>
</div>
</div>
</div>
</div>
</Memo(SettingsGroup)>
<div
className="admin-console-save"
>
<SaveButton
disabled={true}
onClick={[Function]}
saving={false}
savingMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="Saving Config..."
id="admin.saving"
/>
}
/>
<WithTooltip
title=""
<div
class="admin-console-save"
>
<div
className="error-message"
<button
class="btn btn-primary"
data-testid="saveSetting"
disabled=""
id="saveSetting"
type="submit"
>
<Memo(FormError)
error={null}
/>
</div>
</WithTooltip>
<span>
Save
</span>
</button>
<div
class="error-message"
/>
</div>
</div>
</div>
</form>
</form>
</div>
`;
@@ -1,24 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {Client4} from 'mattermost-redux/client';
import {uploadBrandImage, deleteBrandImage} from 'actions/admin_actions.jsx';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import BrandImageSetting from './brand_image_setting';
// Real implementations are async (await dispatch(...)); mocks must return Promises so handleSave can await them.
jest.mock('actions/admin_actions.jsx', () => ({
...jest.requireActual('actions/admin_actions.jsx'),
uploadBrandImage: jest.fn(),
deleteBrandImage: jest.fn(),
uploadBrandImage: jest.fn(async () => {}),
deleteBrandImage: jest.fn(async () => {}),
}));
Client4.setUrl('http://localhost:8065');
describe('components/admin_console/brand_image_setting', () => {
beforeEach(() => {
jest.spyOn(global, 'fetch').mockResolvedValue({status: 404} as Response);
});
afterEach(() => {
jest.restoreAllMocks();
});
const baseProps = {
disabled: false,
setSaveNeeded: jest.fn(),
@@ -26,21 +32,71 @@ describe('components/admin_console/brand_image_setting', () => {
unRegisterSaveAction: jest.fn(),
};
test('should have called deleteBrandImage or uploadBrandImage on save depending on component state', () => {
const wrapper = shallow<BrandImageSetting>(
<BrandImageSetting {...baseProps}/>,
test('should have called deleteBrandImage or uploadBrandImage on save depending on component state', async () => {
let saveAction: (() => Promise<unknown>) | undefined;
const registerSaveAction = jest.fn((fn: () => Promise<unknown>) => {
saveAction = fn;
});
const {container, unmount} = renderWithContext(
<BrandImageSetting
{...baseProps}
registerSaveAction={registerSaveAction}
/>,
);
const instance = wrapper.instance();
// Wait for componentDidMount fetch to resolve
await waitFor(() => {
expect(registerSaveAction).toHaveBeenCalled();
});
expect(saveAction).toBeDefined();
wrapper.setState({deleteBrandImage: false, brandImage: new Blob(['brand_image_file'])});
instance.handleSave();
// Simulate selecting a file via the file input to set brandImage
const file = new File(['brand_image_file'], 'brand.png', {type: 'image/png'});
const fileInput = container.querySelector('input[type="file"]');
expect(fileInput).toBeInTheDocument();
await userEvent.upload(fileInput as HTMLInputElement, file);
// Now call save - should call uploadBrandImage
await saveAction!();
expect(deleteBrandImage).toHaveBeenCalledTimes(0);
expect(uploadBrandImage).toHaveBeenCalledTimes(1);
wrapper.setState({deleteBrandImage: true, brandImage: undefined});
instance.handleSave();
// To test deleteBrandImage path, unmount then re-mount with fetch returning 200
unmount();
jest.clearAllMocks();
(global.fetch as jest.Mock).mockResolvedValueOnce({status: 200} as Response);
let saveAction2: (() => Promise<unknown>) | undefined;
const registerSaveAction2 = jest.fn((fn: () => Promise<unknown>) => {
saveAction2 = fn;
});
renderWithContext(
<BrandImageSetting
{...baseProps}
registerSaveAction={registerSaveAction2}
/>,
);
await waitFor(() => {
expect(registerSaveAction2).toHaveBeenCalled();
});
expect(saveAction2).toBeDefined();
// Wait for the brand image to be detected and delete button to appear
await waitFor(() => {
expect(screen.getByText('×')).toBeInTheDocument();
});
const deleteButton = screen.getByText('×').closest('button')!;
await userEvent.click(deleteButton);
await waitFor(() => {
expect(screen.getByText('No brand image uploaded')).toBeInTheDocument();
});
await saveAction2!();
expect(deleteBrandImage).toHaveBeenCalledTimes(1);
expect(uploadBrandImage).toHaveBeenCalledTimes(1);
expect(uploadBrandImage).toHaveBeenCalledTimes(0);
});
});
@@ -1,42 +1,35 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/MigrationsTable should match snapshot when there are no migrations 1`] = `
<div
className="MigrationsTable migrations-table__panel"
>
<div>
<div
className="help-text"
class="MigrationsTable migrations-table__panel"
>
<MemoizedFormattedMessage
defaultMessage="All applied migrations."
id="admin.database.migrations_table.help_text"
/>
</div>
<div
className="migrations-table__table"
>
<table
className="table"
data-testid="migrationsTable"
<div
class="help-text"
>
<thead>
<tr>
<th>
<MemoizedFormattedMessage
defaultMessage="Version"
id="admin.database.migrations_table.version"
/>
</th>
<th>
<MemoizedFormattedMessage
defaultMessage="Name"
id="admin.database.migrations_table.name"
/>
</th>
</tr>
</thead>
<tbody />
</table>
All applied migrations.
</div>
<div
class="migrations-table__table"
>
<table
class="table"
data-testid="migrationsTable"
>
<thead>
<tr>
<th>
Version
</th>
<th>
Name
</th>
</tr>
</thead>
<tbody />
</table>
</div>
</div>
</div>
`;
@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import {act, screen, waitFor} from '@testing-library/react';
import {shallow} from 'enzyme';
import React from 'react';
import {FormattedMessage} from 'react-intl';
@@ -34,13 +33,18 @@ describe('components/MigrationsTable', () => {
},
};
test('should match snapshot when there are no migrations', () => {
const wrapper = shallow(
test('should match snapshot when there are no migrations', async () => {
const {container} = renderWithContext(
<MigrationsTable
{...baseProps}
/>);
/>,
);
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(baseProps.actions.getAppliedSchemaMigrations).toHaveBeenCalledTimes(1);
});
expect(container).toMatchSnapshot();
});
test('should have called actions.getAppliedSchemaMigrations only when first rendered', async () => {
@@ -1,11 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import DatabaseSettings from 'components/admin_console/database_settings';
import {renderWithContext} from 'tests/react_testing_utils';
jest.mock('actions/admin_actions.jsx', () => {
const pingFn = () => {
return jest.fn(() => {
@@ -47,11 +48,11 @@ describe('components/DatabaseSettings', () => {
config,
isDisabled: false,
};
const wrapper = shallow(
const {container} = renderWithContext(
<DatabaseSettings
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {AdminConfig} from '@mattermost/types/config';
import ElasticSearchSettings from 'components/admin_console/elasticsearch_settings';
import SaveButton from 'components/save_button';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
jest.mock('actions/admin_actions.jsx', () => {
return {
@@ -22,6 +22,7 @@ describe('components/ElasticSearchSettings', () => {
const config = {
ElasticsearchSettings: {
ConnectionURL: 'test',
Backend: '',
SkipTLSVerification: false,
CA: 'test.ca',
ClientCert: 'test.crt',
@@ -32,20 +33,24 @@ describe('components/ElasticSearchSettings', () => {
EnableIndexing: false,
EnableSearching: false,
EnableAutocomplete: false,
EnableSearchPublicChannelsWithoutMembership: false,
IgnoredPurgeIndexes: '',
},
};
const wrapper = shallow(
const {container} = renderWithContext(
<ElasticSearchSettings
config={config as AdminConfig}
isDisabled={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, enabled', () => {
const config = {
ElasticsearchSettings: {
ConnectionURL: 'test',
Backend: '',
SkipTLSVerification: false,
CA: 'test.ca',
ClientCert: 'test.crt',
@@ -56,43 +61,60 @@ describe('components/ElasticSearchSettings', () => {
EnableIndexing: true,
EnableSearching: false,
EnableAutocomplete: false,
EnableSearchPublicChannelsWithoutMembership: false,
IgnoredPurgeIndexes: '',
},
};
const wrapper = shallow(
const {container} = renderWithContext(
<ElasticSearchSettings
config={config as AdminConfig}
isDisabled={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should maintain save disable until is tested', () => {
test('should maintain save disable until is tested', async () => {
const config = {
ElasticsearchSettings: {
ConnectionURL: 'test',
Backend: '',
SkipTLSVerification: false,
CA: '',
ClientCert: '',
ClientKey: '',
Username: 'test',
Password: 'test',
Sniff: false,
EnableIndexing: false,
EnableSearching: false,
EnableAutocomplete: false,
EnableSearchPublicChannelsWithoutMembership: false,
IgnoredPurgeIndexes: '',
},
};
const wrapper = shallow(
renderWithContext(
<ElasticSearchSettings
config={config as AdminConfig}
isDisabled={false}
/>,
);
const instance = wrapper.instance() as any;
expect(wrapper.find(SaveButton).prop('disabled')).toBe(true);
instance.handleSettingChanged('enableIndexing', true);
expect(wrapper.find(SaveButton).prop('disabled')).toBe(true);
const success = jest.fn();
const error = jest.fn();
instance.doTestConfig(success, error);
expect(success).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
expect(wrapper.find(SaveButton).prop('disabled')).toBe(false);
// Save button should be disabled initially (no changes)
expect(screen.getByTestId('saveSetting')).toBeDisabled();
// Enable indexing by clicking the true radio for enableIndexing
const enableIndexingTrue = screen.getByTestId('enableIndexingtrue');
await userEvent.click(enableIndexingTrue);
// Save button should still be disabled because config hasn't been tested
expect(screen.getByTestId('saveSetting')).toBeDisabled();
// Click Test Connection button to test config
const testButton = screen.getByText('Test Connection');
await userEvent.click(testButton);
// After successful test, save button should be enabled
expect(screen.getByTestId('saveSetting')).not.toBeDisabled();
});
});
@@ -1,149 +1,73 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ManageTeamsDropdown should match snapshot for guest 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
>
<a>
<span>
Guest
</span>
<span
className="caret"
/>
</a>
<Menu
ariaLabel="Change the role of a team member"
openLeft={true}
<div>
<div
class="MenuWrapper "
>
<MenuItemAction
onClick={[Function]}
show={false}
text="Make Team Admin"
/>
<MenuItemAction
onClick={[Function]}
show={false}
text="Make Team Member"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Remove from Team"
/>
</Menu>
</MenuWrapper>
<a>
<span>
Guest
</span>
<span
class="caret"
/>
</a>
</div>
</div>
`;
exports[`ManageTeamsDropdown should match snapshot for system admin 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
>
<a>
<span>
System Admin
</span>
<span
className="caret"
/>
</a>
<Menu
ariaLabel="Change the role of a team member"
openLeft={true}
<div>
<div
class="MenuWrapper "
>
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Team Admin"
/>
<MenuItemAction
onClick={[Function]}
show={false}
text="Make Team Member"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Remove from Team"
/>
</Menu>
</MenuWrapper>
<a>
<span>
System Admin
</span>
<span
class="caret"
/>
</a>
</div>
</div>
`;
exports[`ManageTeamsDropdown should match snapshot for team admin 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
>
<a>
<span>
Team Admin
</span>
<span
className="caret"
/>
</a>
<Menu
ariaLabel="Change the role of a team member"
openLeft={true}
<div>
<div
class="MenuWrapper "
>
<MenuItemAction
onClick={[Function]}
show={false}
text="Make Team Admin"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Team Member"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Remove from Team"
/>
</Menu>
</MenuWrapper>
<a>
<span>
Team Admin
</span>
<span
class="caret"
/>
</a>
</div>
</div>
`;
exports[`ManageTeamsDropdown should match snapshot for team member 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
>
<a>
<span>
Team Member
</span>
<span
className="caret"
/>
</a>
<Menu
ariaLabel="Change the role of a team member"
openLeft={true}
<div>
<div
class="MenuWrapper "
>
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Team Admin"
/>
<MenuItemAction
onClick={[Function]}
show={false}
text="Make Team Member"
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Remove from Team"
/>
</Menu>
</MenuWrapper>
<a>
<span>
Team Member
</span>
<span
class="caret"
/>
</a>
</div>
</div>
`;
@@ -1,85 +1,95 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ManageTeamsModal should match snapshot init 1`] = `
<Modal
animation={true}
aria-labelledby="manageTeamsModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal manage-teams modal--overflow-visible"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
<body
class="modal-open"
style="padding-right: 0px;"
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
<div
aria-hidden="true"
/>
<div
role="none"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="manageTeamsModalLabel"
<div
class="fade modal-backdrop in"
/>
<div
aria-labelledby="manageTeamsModalLabel"
class="fade in modal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<MemoizedFormattedMessage
defaultMessage="Manage Teams"
id="admin.user_item.manageTeams"
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div>
<div
className="manage-teams__user"
class="a11y__modal manage-teams modal--overflow-visible modal-dialog"
>
<Memo(Avatar)
size="lg"
url="/api/v4/users/currentUserId/image?_=1234"
username="currentUsername"
/>
<div
className="manage-teams__info"
class="modal-content"
role="document"
>
<div
className="manage-teams__name"
class="modal-header"
>
@currentUsername
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h1
class="modal-title"
id="manageTeamsModalLabel"
>
Manage Teams
</h1>
</div>
<div
className="manage-teams__email"
class="modal-body"
>
currentUser@test.com
<div>
<div
class="manage-teams__user"
>
<img
alt="currentUsername profile image"
class="Avatar Avatar-lg"
loading="lazy"
src="/api/v4/users/currentUserId/image?_=1234"
/>
<div
class="manage-teams__info"
>
<div
class="manage-teams__name"
>
@currentUsername
</div>
<div
class="manage-teams__email"
>
currentUser@test.com
</div>
</div>
</div>
<div
class="manage-teams__teams"
/>
</div>
</div>
</div>
</div>
<div
className="manage-teams__teams"
>
<LoadingScreen />
</div>
</div>
</ModalBody>
</Modal>
</div>
</body>
`;
@@ -1,14 +1,12 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`RemoveFromTeamButton should match snapshot init 1`] = `
<button
className="btn btn-danger"
onClick={[Function]}
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Remove from Team"
id="team_members_dropdown.leave_team"
/>
</button>
<div>
<button
class="btn btn-danger"
type="button"
>
Remove from Team
</button>
</div>
`;
@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import ManageTeamsDropdown from 'components/admin_console/manage_teams_modal/manage_teams_dropdown';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
describe('ManageTeamsDropdown', () => {
@@ -31,11 +31,12 @@ describe('ManageTeamsDropdown', () => {
};
test('should match snapshot for team member', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<ManageTeamsDropdown {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Team Member')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot for system admin', () => {
@@ -49,11 +50,12 @@ describe('ManageTeamsDropdown', () => {
user,
};
const wrapper = shallow(
const {container} = renderWithContext(
<ManageTeamsDropdown {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('System Admin')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot for team admin', () => {
@@ -73,11 +75,12 @@ describe('ManageTeamsDropdown', () => {
teamMember,
};
const wrapper = shallow(
const {container} = renderWithContext(
<ManageTeamsDropdown {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Team Admin')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot for guest', () => {
@@ -96,10 +99,11 @@ describe('ManageTeamsDropdown', () => {
teamMember,
};
const wrapper = shallow(
const {container} = renderWithContext(
<ManageTeamsDropdown {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Guest')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});
@@ -1,20 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ReactWrapper} from 'enzyme';
import {mount, shallow} from 'enzyme';
import React from 'react';
import {IntlProvider} from 'react-intl';
import {General} from 'mattermost-redux/constants';
import ManageTeamsModal from 'components/admin_console/manage_teams_modal/manage_teams_modal';
import {act} from 'tests/react_testing_utils';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ManageTeamsDropdown from './manage_teams_dropdown';
describe('ManageTeamsModal', () => {
const baseProps = {
locale: General.DEFAULT_LOCALE,
@@ -35,30 +30,33 @@ describe('ManageTeamsModal', () => {
},
};
test('should match snapshot init', () => {
const wrapper = shallow(<ManageTeamsModal {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
});
test('should match snapshot init', async () => {
const {baseElement} = renderWithContext(<ManageTeamsModal {...baseProps}/>);
await waitFor(() => {
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
});
expect(screen.getByText('Manage Teams')).toBeInTheDocument();
expect(screen.getByText('@currentUsername')).toBeInTheDocument();
expect(screen.getByText('currentUser@test.com')).toBeInTheDocument();
expect(baseElement).toMatchSnapshot();
});
test('should call api calls on mount', async () => {
const intlProviderProps = {
defaultLocale: 'en',
locale: 'en',
messages: {testId: 'Actual value'},
};
renderWithContext(<ManageTeamsModal {...baseProps}/>);
await act(async () => {
mount(
<IntlProvider {...intlProviderProps}>
<ManageTeamsModal {...baseProps}/>
</IntlProvider>,
);
await waitFor(() => {
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledWith(baseProps.user.id);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledWith(baseProps.user.id);
});
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamMembersForUser).toHaveBeenCalledWith(baseProps.user.id);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledTimes(1);
expect(baseProps.actions.getTeamsForUser).toHaveBeenCalledWith(baseProps.user.id);
});
test('should save data in state from api calls', async () => {
@@ -80,23 +78,11 @@ describe('ManageTeamsModal', () => {
getTeamsForUser,
},
};
const intlProviderProps = {
defaultLocale: 'en',
locale: 'en',
messages: {'test.value': 'Actual value'},
};
let wrapper: ReactWrapper<any>;
await act(async () => {
wrapper = mount(
<IntlProvider {...intlProviderProps}>
<ManageTeamsModal {...props}/>
</IntlProvider>,
);
renderWithContext(<ManageTeamsModal {...props}/>);
await waitFor(() => {
expect(screen.getByText(mockTeamData.display_name)).toBeInTheDocument();
});
wrapper!.update();
expect(wrapper!.find('.manage-teams__team-name').text()).toEqual(mockTeamData.display_name);
expect(wrapper!.find(ManageTeamsDropdown).props().teamMember).toEqual({team_id: '123test'});
});
});
@@ -1,11 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import RemoveFromTeamButton from 'components/admin_console/manage_teams_modal/remove_from_team_button';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
describe('RemoveFromTeamButton', () => {
const baseProps = {
teamId: '1234',
@@ -13,18 +14,19 @@ describe('RemoveFromTeamButton', () => {
};
test('should match snapshot init', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<RemoveFromTeamButton {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByRole('button', {name: 'Remove from Team'})).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should call handleRemoveUserFromTeam on button click', () => {
const wrapper = shallow(
test('should call handleRemoveUserFromTeam on button click', async () => {
renderWithContext(
<RemoveFromTeamButton {...baseProps}/>,
);
wrapper.find('button').prop('onClick')!({preventDefault: jest.fn()} as unknown as React.MouseEvent);
await userEvent.click(screen.getByRole('button', {name: 'Remove from Team'}));
expect(baseProps.handleRemoveUserFromTeam).toHaveBeenCalledTimes(1);
});
});
@@ -1,365 +1,435 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/admin_console/manage_tokens_modal/manage_tokens_modal.tsx initial call should match snapshot 1`] = `
<Modal
animation={true}
aria-labelledby="manageTokensModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal manage-teams"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
<body
class="modal-open"
style="padding-right: 0px;"
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
<div
aria-hidden="true"
/>
<div
role="none"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="manageTokensModalLabel"
<div
class="fade modal-backdrop in"
/>
<div
aria-labelledby="manageTokensModalLabel"
class="fade in modal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<MemoizedFormattedMessage
defaultMessage="Manage Personal Access Tokens"
id="admin.manage_tokens.manageTokensTitle"
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div>
<div
className="manage-teams__user"
class="a11y__modal manage-teams modal-dialog"
>
<Memo(Avatar)
size="lg"
url="/api/v4/users/defaultuser/image"
username="some-user"
/>
<div
className="manage-teams__info"
class="modal-content"
role="document"
>
<div
className="manage-teams__name"
class="modal-header"
>
@some-user
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h1
class="modal-title"
id="manageTokensModalLabel"
>
Manage Personal Access Tokens
</h1>
</div>
<div
className="manage-teams__email"
/>
class="modal-body"
>
<div>
<div
class="manage-teams__user"
>
<img
alt="some-user profile image"
class="Avatar Avatar-lg"
loading="lazy"
src="/api/v4/users/defaultuser/image"
/>
<div
class="manage-teams__info"
>
<div
class="manage-teams__name"
>
@some-user
</div>
<div
class="manage-teams__email"
/>
</div>
</div>
<div
class="pt-3"
>
Personal access tokens function similarly to session tokens and can be used by integrations to
<a
href="https://api.mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=manage_tokens_modal&uid=&sid=&edition=team&server_version=#tag/authentication"
location="manage_tokens_modal"
rel="noopener noreferrer"
target="_blank"
>
interact with this Mattermost server
</a>
. Tokens are disabled if the user is deactivated. Learn more about
<a
href="https://mattermost.com/pl/personal-access-tokens?utm_source=mattermost&utm_medium=in-product&utm_content=manage_tokens_modal&uid=&sid=&edition=team&server_version="
location="manage_tokens_modal"
rel="noopener noreferrer"
target="_blank"
>
personal access tokens
</a>
.
</div>
<div
class="manage-teams__teams"
>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="pt-3"
>
<MemoizedFormattedMessage
defaultMessage="Personal access tokens function similarly to session tokens and can be used by integrations to <linkAuthentication>interact with this Mattermost server</linkAuthentication>. Tokens are disabled if the user is deactivated. Learn more about <linkPersonalAccessTokens>personal access tokens</linkPersonalAccessTokens>."
id="admin.manage_tokens.userAccessTokensDescription"
values={
Object {
"linkAuthentication": [Function],
"linkPersonalAccessTokens": [Function],
}
}
/>
</div>
<div
className="manage-teams__teams"
>
<LoadingScreen />
</div>
</div>
</ModalBody>
</Modal>
</div>
</body>
`;
exports[`components/admin_console/manage_tokens_modal/manage_tokens_modal.tsx should display list of tokens 1`] = `
<Modal
animation={true}
aria-labelledby="manageTokensModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal manage-teams"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
<body
class="modal-open"
style="padding-right: 0px;"
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
<div
aria-hidden="true"
/>
<div
role="none"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="manageTokensModalLabel"
<div
class="fade modal-backdrop in"
/>
<div
aria-labelledby="manageTokensModalLabel"
class="fade in modal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<MemoizedFormattedMessage
defaultMessage="Manage Personal Access Tokens"
id="admin.manage_tokens.manageTokensTitle"
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div>
<div
className="manage-teams__user"
>
<Memo(Avatar)
size="lg"
url="/api/v4/users/defaultuser/image"
username="some-user"
/>
<div
className="manage-teams__info"
>
<div
className="manage-teams__name"
>
@some-user
</div>
<div
className="manage-teams__email"
/>
</div>
</div>
<div
className="pt-3"
>
<MemoizedFormattedMessage
defaultMessage="Personal access tokens function similarly to session tokens and can be used by integrations to <linkAuthentication>interact with this Mattermost server</linkAuthentication>. Tokens are disabled if the user is deactivated. Learn more about <linkPersonalAccessTokens>personal access tokens</linkPersonalAccessTokens>."
id="admin.manage_tokens.userAccessTokensDescription"
values={
Object {
"linkAuthentication": [Function],
"linkPersonalAccessTokens": [Function],
}
}
/>
</div>
<div
className="manage-teams__teams"
class="a11y__modal manage-teams modal-dialog"
>
<div
className="manage-teams__team"
key="id1"
class="modal-content"
role="document"
>
<div
className="manage-teams__team-name"
class="modal-header"
>
<div
className="whitespace--nowrap overflow--ellipsis"
<button
aria-label="Close"
class="close"
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Token Description: "
id="admin.manage_tokens.userAccessTokensNameLabel"
/>
description
</div>
<div
className="whitespace--nowrap overflow--ellipsis"
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h1
class="modal-title"
id="manageTokensModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="Token ID: "
id="admin.manage_tokens.userAccessTokensIdLabel"
/>
id1
</div>
Manage Personal Access Tokens
</h1>
</div>
<div
className="manage-teams__team-actions"
class="modal-body"
>
<Connect(Component)
onError={[Function]}
tokenId="id1"
/>
</div>
</div>
<div
className="manage-teams__team"
key="id2"
>
<div
className="manage-teams__team-name"
>
<div
className="whitespace--nowrap overflow--ellipsis"
>
<MemoizedFormattedMessage
defaultMessage="Token Description: "
id="admin.manage_tokens.userAccessTokensNameLabel"
/>
description
<div>
<div
class="manage-teams__user"
>
<img
alt="some-user profile image"
class="Avatar Avatar-lg"
loading="lazy"
src="/api/v4/users/defaultuser/image"
/>
<div
class="manage-teams__info"
>
<div
class="manage-teams__name"
>
@some-user
</div>
<div
class="manage-teams__email"
/>
</div>
</div>
<div
class="pt-3"
>
Personal access tokens function similarly to session tokens and can be used by integrations to
<a
href="https://api.mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=manage_tokens_modal&uid=&sid=&edition=team&server_version=#tag/authentication"
location="manage_tokens_modal"
rel="noopener noreferrer"
target="_blank"
>
interact with this Mattermost server
</a>
. Tokens are disabled if the user is deactivated. Learn more about
<a
href="https://mattermost.com/pl/personal-access-tokens?utm_source=mattermost&utm_medium=in-product&utm_content=manage_tokens_modal&uid=&sid=&edition=team&server_version="
location="manage_tokens_modal"
rel="noopener noreferrer"
target="_blank"
>
personal access tokens
</a>
.
</div>
<div
class="manage-teams__teams"
>
<div
class="manage-teams__team"
>
<div
class="manage-teams__team-name"
>
<div
class="whitespace--nowrap overflow--ellipsis"
>
Token Description:
first token
</div>
<div
class="whitespace--nowrap overflow--ellipsis"
>
Token ID:
id1
</div>
</div>
<div
class="manage-teams__team-actions"
>
<button
class="btn btn-danger"
type="button"
>
Delete
</button>
</div>
</div>
<div
class="manage-teams__team"
>
<div
class="manage-teams__team-name"
>
<div
class="whitespace--nowrap overflow--ellipsis"
>
Token Description:
second token
</div>
<div
class="whitespace--nowrap overflow--ellipsis"
>
Token ID:
id2
</div>
</div>
<div
class="manage-teams__team-actions"
>
<button
class="btn btn-danger"
type="button"
>
Delete
</button>
</div>
</div>
</div>
</div>
<div
className="whitespace--nowrap overflow--ellipsis"
>
<MemoizedFormattedMessage
defaultMessage="Token ID: "
id="admin.manage_tokens.userAccessTokensIdLabel"
/>
id2
</div>
</div>
<div
className="manage-teams__team-actions"
>
<Connect(Component)
onError={[Function]}
tokenId="id2"
/>
</div>
</div>
</div>
</div>
</ModalBody>
</Modal>
</div>
</body>
`;
exports[`components/admin_console/manage_tokens_modal/manage_tokens_modal.tsx should replace loading screen on update 1`] = `
<Modal
animation={true}
aria-labelledby="manageTokensModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal manage-teams"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
<body
class="modal-open"
style="padding-right: 0px;"
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
<div
aria-hidden="true"
/>
<div
role="none"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="manageTokensModalLabel"
<div
class="fade modal-backdrop in"
/>
<div
aria-labelledby="manageTokensModalLabel"
class="fade in modal"
role="dialog"
style="display: block; padding-right: 0px;"
tabindex="-1"
>
<MemoizedFormattedMessage
defaultMessage="Manage Personal Access Tokens"
id="admin.manage_tokens.manageTokensTitle"
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div>
<div
className="manage-teams__user"
class="a11y__modal manage-teams modal-dialog"
>
<Memo(Avatar)
size="lg"
url="/api/v4/users/defaultuser/image"
username="some-user"
/>
<div
className="manage-teams__info"
class="modal-content"
role="document"
>
<div
className="manage-teams__name"
class="modal-header"
>
@some-user
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<h1
class="modal-title"
id="manageTokensModalLabel"
>
Manage Personal Access Tokens
</h1>
</div>
<div
className="manage-teams__email"
/>
</div>
</div>
<div
className="pt-3"
>
<MemoizedFormattedMessage
defaultMessage="Personal access tokens function similarly to session tokens and can be used by integrations to <linkAuthentication>interact with this Mattermost server</linkAuthentication>. Tokens are disabled if the user is deactivated. Learn more about <linkPersonalAccessTokens>personal access tokens</linkPersonalAccessTokens>."
id="admin.manage_tokens.userAccessTokensDescription"
values={
Object {
"linkAuthentication": [Function],
"linkPersonalAccessTokens": [Function],
}
}
/>
</div>
<div
className="manage-teams__teams"
>
<div
className="manage-row__empty"
>
<MemoizedFormattedMessage
defaultMessage="No personal access tokens."
id="admin.manage_tokens.userAccessTokensNone"
/>
class="modal-body"
>
<div>
<div
class="manage-teams__user"
>
<img
alt="some-user profile image"
class="Avatar Avatar-lg"
loading="lazy"
src="/api/v4/users/defaultuser/image"
/>
<div
class="manage-teams__info"
>
<div
class="manage-teams__name"
>
@some-user
</div>
<div
class="manage-teams__email"
/>
</div>
</div>
<div
class="pt-3"
>
Personal access tokens function similarly to session tokens and can be used by integrations to
<a
href="https://api.mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=manage_tokens_modal&uid=&sid=&edition=team&server_version=#tag/authentication"
location="manage_tokens_modal"
rel="noopener noreferrer"
target="_blank"
>
interact with this Mattermost server
</a>
. Tokens are disabled if the user is deactivated. Learn more about
<a
href="https://mattermost.com/pl/personal-access-tokens?utm_source=mattermost&utm_medium=in-product&utm_content=manage_tokens_modal&uid=&sid=&edition=team&server_version="
location="manage_tokens_modal"
rel="noopener noreferrer"
target="_blank"
>
personal access tokens
</a>
.
</div>
<div
class="manage-teams__teams"
>
<div
class="manage-row__empty"
>
No personal access tokens.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</ModalBody>
</Modal>
</div>
</body>
`;
@@ -1,11 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import LoadingScreen from 'components/loading_screen';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ManageTokensModal from './manage_tokens_modal';
@@ -23,50 +21,67 @@ describe('components/admin_console/manage_tokens_modal/manage_tokens_modal.tsx',
};
test('initial call should match snapshot', () => {
const wrapper = shallow(
const {baseElement} = renderWithContext(
<ManageTokensModal {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(baseProps.actions.getUserAccessTokensForUser).toHaveBeenCalledTimes(1);
expect(wrapper.find('.manage-teams__teams').exists()).toBe(true);
expect(wrapper.find(LoadingScreen).exists()).toBe(true);
expect(screen.getByText('Manage Personal Access Tokens')).toBeInTheDocument();
expect(baseElement.querySelector('.manage-teams__teams')).toBeInTheDocument();
expect(baseElement).toMatchSnapshot();
});
test('should replace loading screen on update', () => {
const wrapper = shallow(
const {baseElement, rerender} = renderWithContext(
<ManageTokensModal {...baseProps}/>,
);
const newProps = {
...baseProps,
userAccessTokens: {},
};
wrapper.setProps(newProps);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.manage-teams__teams').exists()).toBe(true);
expect(wrapper.find('.manage-row__empty').exists()).toBe(true);
expect(screen.queryByText('No personal access tokens.')).not.toBeInTheDocument();
expect(baseElement.querySelector('.loading-screen')).toBeInTheDocument();
expect(baseElement.querySelector('.manage-row__empty')).not.toBeInTheDocument();
rerender(
<ManageTokensModal
{...baseProps}
userAccessTokens={{}}
/>,
);
expect(baseElement.querySelector('.manage-teams__teams')).toBeInTheDocument();
expect(baseElement.querySelector('.loading-screen')).not.toBeInTheDocument();
expect(baseElement.querySelector('.manage-row__empty')).toBeInTheDocument();
expect(screen.getByText('No personal access tokens.')).toBeInTheDocument();
expect(baseElement).toMatchSnapshot();
});
test('should display list of tokens', () => {
const wrapper = shallow(
<ManageTokensModal {...baseProps}/>,
);
const newProps = {
...baseProps,
userAccessTokens: [
{
id: 'id1',
description: 'description',
},
{
id: 'id2',
description: 'description',
},
],
const userAccessTokens = {
id1: {
id: 'id1',
description: 'first token',
user_id: 'defaultuser',
is_active: true,
},
id2: {
id: 'id2',
description: 'second token',
user_id: 'defaultuser',
is_active: true,
},
};
wrapper.setProps(newProps);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.manage-teams__teams').exists()).toBe(true);
expect(wrapper.find('.manage-teams__team').length).toBe(2);
const {baseElement} = renderWithContext(
<ManageTokensModal
{...baseProps}
userAccessTokens={userAccessTokens}
/>,
);
expect(baseElement.querySelector('.manage-teams__teams')).toBeInTheDocument();
expect(baseElement.querySelectorAll('.manage-teams__team')).toHaveLength(2);
expect(screen.getByText(/first token/)).toBeInTheDocument();
expect(screen.getByText(/second token/)).toBeInTheDocument();
expect(screen.getByText(/id1/)).toBeInTheDocument();
expect(screen.getByText(/id2/)).toBeInTheDocument();
expect(baseElement).toMatchSnapshot();
});
});
@@ -1,633 +1,350 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`admin_console/system_user_detail/team_list/AbstractList should match snapshot if loading 1`] = `
<div
className="AbstractList"
>
<div>
<div
className="AbstractList__body"
class="AbstractList"
>
<div
className="AbstractList__loading"
class="AbstractList__body"
>
<i
className="fa fa-spinner fa-pulse fa-2x"
/>
<div
class="AbstractList__loading"
>
<i
class="fa fa-spinner fa-pulse fa-2x"
/>
</div>
</div>
</div>
</div>
`;
exports[`admin_console/system_user_detail/team_list/AbstractList should match snapshot no data 1`] = `
<div
className="AbstractList"
>
<div>
<div
className="AbstractList__body"
class="AbstractList"
>
<div
className="AbstractList__empty"
class="AbstractList__body"
>
<MemoizedFormattedMessage
defaultMessage="No teams found"
id="admin.team_settings.team_list.no_teams_found"
/>
<div
class="AbstractList__empty"
>
No teams found
</div>
</div>
</div>
</div>
`;
exports[`admin_console/system_user_detail/team_list/AbstractList should match snapshot when on second page of pagination 1`] = `
<div
className="AbstractList"
>
<div>
<div
className="AbstractList__header"
class="AbstractList"
>
<div
className="AbstractList__header-label"
key="0"
style={
Object {
"flexGrow": 1,
"marginLeft": "16px",
"minWidth": "284px",
}
}
class="AbstractList__header"
>
<MemoizedFormattedMessage
defaultMessage="Name"
id="admin.team_settings.team_list.header.name"
<div
class="AbstractList__header-label"
style="flex-grow: 1; min-width: 284px; margin-left: 16px;"
>
Name
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Type
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Role
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
/>
</div>
<div
className="AbstractList__header-label"
key="1"
style={
Object {
"width": "150px",
}
}
class="AbstractList__body"
>
<MemoizedFormattedMessage
defaultMessage="Type"
id="admin.systemUserDetail.teamList.header.type"
/>
<div
data-testid="team-row-id11"
>
Team 11
</div>
<div
data-testid="team-row-id12"
>
Team 12
</div>
<div
data-testid="team-row-id13"
>
Team 13
</div>
<div
data-testid="team-row-id14"
>
Team 14
</div>
<div
data-testid="team-row-id15"
>
Team 15
</div>
<div
data-testid="team-row-id16"
>
Team 16
</div>
<div
data-testid="team-row-id17"
>
Team 17
</div>
<div
data-testid="team-row-id18"
>
Team 18
</div>
<div
data-testid="team-row-id19"
>
Team 19
</div>
<div
data-testid="team-row-id20"
>
Team 20
</div>
</div>
<div
className="AbstractList__header-label"
key="2"
style={
Object {
"width": "150px",
}
}
class="AbstractList__footer"
>
<MemoizedFormattedMessage
defaultMessage="Role"
id="admin.systemUserDetail.teamList.header.role"
/>
<div
class="counter"
>
11 - 20 of 30
</div>
<button
class="btn btn-tertiary prev "
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
class="btn btn-tertiary next "
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
<div
className="AbstractList__header-label"
key="3"
style={
Object {
"width": "150px",
}
}
/>
</div>
<div
className="AbstractList__body"
>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id11"
team={
Object {
"description": "Team 11 description",
"display_name": "Team 11",
"id": "id11",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id12"
team={
Object {
"description": "Team 12 description",
"display_name": "Team 12",
"id": "id12",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id13"
team={
Object {
"description": "Team 13 description",
"display_name": "Team 13",
"id": "id13",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id14"
team={
Object {
"description": "Team 14 description",
"display_name": "Team 14",
"id": "id14",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id15"
team={
Object {
"description": "Team 15 description",
"display_name": "Team 15",
"id": "id15",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id16"
team={
Object {
"description": "Team 16 description",
"display_name": "Team 16",
"id": "id16",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id17"
team={
Object {
"description": "Team 17 description",
"display_name": "Team 17",
"id": "id17",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id18"
team={
Object {
"description": "Team 18 description",
"display_name": "Team 18",
"id": "id18",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id19"
team={
Object {
"description": "Team 19 description",
"display_name": "Team 19",
"id": "id19",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id20"
team={
Object {
"description": "Team 20 description",
"display_name": "Team 20",
"id": "id20",
}
}
/>
</div>
<div
className="AbstractList__footer"
>
<div
className="counter"
>
<MemoizedFormattedMessage
defaultMessage="{startCount, number} - {endCount, number} of {total, number}"
id="admin.team_channel_settings.list.paginatorCount"
values={
Object {
"endCount": 20,
"startCount": 11,
"total": 30,
}
}
/>
</div>
<button
className="btn btn-tertiary prev "
disabled={false}
onClick={[Function]}
type="button"
>
<Memo(PreviousIcon) />
</button>
<button
className="btn btn-tertiary next "
disabled={false}
onClick={[Function]}
type="button"
>
<Memo(NextIcon) />
</button>
</div>
</div>
`;
exports[`admin_console/system_user_detail/team_list/AbstractList should match snapshot with enough teams data to require paging 1`] = `
<div
className="AbstractList"
>
<div>
<div
className="AbstractList__header"
class="AbstractList"
>
<div
className="AbstractList__header-label"
key="0"
style={
Object {
"flexGrow": 1,
"marginLeft": "16px",
"minWidth": "284px",
}
}
class="AbstractList__header"
>
<MemoizedFormattedMessage
defaultMessage="Name"
id="admin.team_settings.team_list.header.name"
<div
class="AbstractList__header-label"
style="flex-grow: 1; min-width: 284px; margin-left: 16px;"
>
Name
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Type
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Role
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
/>
</div>
<div
className="AbstractList__header-label"
key="1"
style={
Object {
"width": "150px",
}
}
class="AbstractList__body"
>
<MemoizedFormattedMessage
defaultMessage="Type"
id="admin.systemUserDetail.teamList.header.type"
/>
<div
data-testid="team-row-id1"
>
Team 1
</div>
<div
data-testid="team-row-id2"
>
Team 2
</div>
<div
data-testid="team-row-id3"
>
Team 3
</div>
<div
data-testid="team-row-id4"
>
Team 4
</div>
<div
data-testid="team-row-id5"
>
Team 5
</div>
<div
data-testid="team-row-id6"
>
Team 6
</div>
<div
data-testid="team-row-id7"
>
Team 7
</div>
<div
data-testid="team-row-id8"
>
Team 8
</div>
<div
data-testid="team-row-id9"
>
Team 9
</div>
<div
data-testid="team-row-id10"
>
Team 10
</div>
</div>
<div
className="AbstractList__header-label"
key="2"
style={
Object {
"width": "150px",
}
}
class="AbstractList__footer"
>
<MemoizedFormattedMessage
defaultMessage="Role"
id="admin.systemUserDetail.teamList.header.role"
/>
<div
class="counter"
>
1 - 10 of 30
</div>
<button
class="btn btn-tertiary prev disabled"
disabled=""
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
class="btn btn-tertiary next "
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
<div
className="AbstractList__header-label"
key="3"
style={
Object {
"width": "150px",
}
}
/>
</div>
<div
className="AbstractList__body"
>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id1"
team={
Object {
"description": "Team 1 description",
"display_name": "Team 1",
"id": "id1",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id2"
team={
Object {
"description": "The 2 description",
"display_name": "Team 2",
"id": "id2",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id3"
team={
Object {
"description": "Team 3 description",
"display_name": "Team 3",
"id": "id3",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id4"
team={
Object {
"description": "Team 4 description",
"display_name": "Team 4",
"id": "id4",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id5"
team={
Object {
"description": "Team 5 description",
"display_name": "Team 5",
"id": "id5",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id6"
team={
Object {
"description": "Team 6 description",
"display_name": "Team 6",
"id": "id6",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id7"
team={
Object {
"description": "Team 7 description",
"display_name": "Team 7",
"id": "id7",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id8"
team={
Object {
"description": "Team 8 description",
"display_name": "Team 8",
"id": "id8",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id9"
team={
Object {
"description": "Team 9 description",
"display_name": "Team 9",
"id": "id9",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id10"
team={
Object {
"description": "Team 10 description",
"display_name": "Team 10",
"id": "id10",
}
}
/>
</div>
<div
className="AbstractList__footer"
>
<div
className="counter"
>
<MemoizedFormattedMessage
defaultMessage="{startCount, number} - {endCount, number} of {total, number}"
id="admin.team_channel_settings.list.paginatorCount"
values={
Object {
"endCount": 10,
"startCount": 1,
"total": 30,
}
}
/>
</div>
<button
className="btn btn-tertiary prev disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<Memo(PreviousIcon) />
</button>
<button
className="btn btn-tertiary next "
disabled={false}
onClick={[Function]}
type="button"
>
<Memo(NextIcon) />
</button>
</div>
</div>
`;
exports[`admin_console/system_user_detail/team_list/AbstractList should match snapshot with teams data populated 1`] = `
<div
className="AbstractList"
>
<div>
<div
className="AbstractList__header"
class="AbstractList"
>
<div
className="AbstractList__header-label"
key="0"
style={
Object {
"flexGrow": 1,
"marginLeft": "16px",
"minWidth": "284px",
}
}
class="AbstractList__header"
>
<MemoizedFormattedMessage
defaultMessage="Name"
id="admin.team_settings.team_list.header.name"
<div
class="AbstractList__header-label"
style="flex-grow: 1; min-width: 284px; margin-left: 16px;"
>
Name
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Type
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Role
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
/>
</div>
<div
className="AbstractList__header-label"
key="1"
style={
Object {
"width": "150px",
}
}
class="AbstractList__body"
>
<MemoizedFormattedMessage
defaultMessage="Type"
id="admin.systemUserDetail.teamList.header.type"
/>
<div
data-testid="team-row-id1"
>
Team 1
</div>
<div
data-testid="team-row-id2"
>
Team 2
</div>
</div>
<div
className="AbstractList__header-label"
key="2"
style={
Object {
"width": "150px",
}
}
class="AbstractList__footer"
>
<MemoizedFormattedMessage
defaultMessage="Role"
id="admin.systemUserDetail.teamList.header.role"
/>
<div
class="counter"
>
1 - 2 of 2
</div>
<button
class="btn btn-tertiary prev disabled"
disabled=""
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
class="btn btn-tertiary next disabled"
disabled=""
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
<div
className="AbstractList__header-label"
key="3"
style={
Object {
"width": "150px",
}
}
/>
</div>
<div
className="AbstractList__body"
>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id1"
team={
Object {
"description": "Team 1 description",
"display_name": "Team 1",
"id": "id1",
}
}
/>
<TeamRow
doMakeUserTeamAdmin={[MockFunction]}
doMakeUserTeamMember={[MockFunction]}
doRemoveUserFromTeam={[MockFunction]}
key="id2"
team={
Object {
"description": "The 2 description",
"display_name": "Team 2",
"id": "id2",
}
}
/>
</div>
<div
className="AbstractList__footer"
>
<div
className="counter"
>
<MemoizedFormattedMessage
defaultMessage="{startCount, number} - {endCount, number} of {total, number}"
id="admin.team_channel_settings.list.paginatorCount"
values={
Object {
"endCount": 2,
"startCount": 1,
"total": 2,
}
}
/>
</div>
<button
className="btn btn-tertiary prev disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<Memo(PreviousIcon) />
</button>
<button
className="btn btn-tertiary next disabled"
disabled={true}
onClick={[Function]}
type="button"
>
<Memo(NextIcon) />
</button>
</div>
</div>
`;
@@ -1,142 +1,99 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`admin_console/system_user_detail/team_list/TeamList should match snapshot when no teams are found 1`] = `
<Fragment>
<div>
<div />
<AbstractList
actions={
Object {
"getTeamMembersForUser": [Function],
"getTeamsData": [Function],
"removeUserFromTeam": [MockFunction],
"updateTeamMemberSchemeRoles": [MockFunction],
}
}
data={Array []}
emptyList={
Object {
"defaultMessage": "No teams found",
"id": "admin.team_settings.team_list.no_teams_found",
}
}
headerLabels={
Array [
Object {
"label": Object {
"defaultMessage": "Name",
"id": "admin.systemUserDetail.teamList.header.name",
},
"style": Object {
"flexGrow": 1,
"marginLeft": "16px",
"minWidth": "284px",
},
},
Object {
"label": Object {
"defaultMessage": "Type",
"id": "admin.systemUserDetail.teamList.header.type",
},
"style": Object {
"width": "150px",
},
},
Object {
"label": Object {
"defaultMessage": "Role",
"id": "admin.systemUserDetail.teamList.header.role",
},
"style": Object {
"width": "150px",
},
},
Object {
"style": Object {
"width": "150px",
},
},
]
}
renderRow={[Function]}
total={0}
userId="1234"
/>
</Fragment>
<div
class="AbstractList"
>
<div
class="AbstractList__body"
>
<div
class="AbstractList__empty"
>
No teams found
</div>
</div>
</div>
</div>
`;
exports[`admin_console/system_user_detail/team_list/TeamList should match snapshot with teams populated 1`] = `
<Fragment>
<div>
<div />
<AbstractList
actions={
Object {
"getTeamMembersForUser": [Function],
"getTeamsData": [Function],
"removeUserFromTeam": [MockFunction],
"updateTeamMemberSchemeRoles": [MockFunction],
}
}
data={
Array [
Object {
"description": "Team 1 description",
"display_name": "Team 1",
"id": "id1",
},
Object {
"description": "The 2 description",
"display_name": "Team 2",
"id": "id2",
},
]
}
emptyList={
Object {
"defaultMessage": "No teams found",
"id": "admin.team_settings.team_list.no_teams_found",
}
}
headerLabels={
Array [
Object {
"label": Object {
"defaultMessage": "Name",
"id": "admin.systemUserDetail.teamList.header.name",
},
"style": Object {
"flexGrow": 1,
"marginLeft": "16px",
"minWidth": "284px",
},
},
Object {
"label": Object {
"defaultMessage": "Type",
"id": "admin.systemUserDetail.teamList.header.type",
},
"style": Object {
"width": "150px",
},
},
Object {
"label": Object {
"defaultMessage": "Role",
"id": "admin.systemUserDetail.teamList.header.role",
},
"style": Object {
"width": "150px",
},
},
Object {
"style": Object {
"width": "150px",
},
},
]
}
renderRow={[Function]}
total={2}
userId="1234"
/>
</Fragment>
<div
class="AbstractList"
>
<div
class="AbstractList__header"
>
<div
class="AbstractList__header-label"
style="flex-grow: 1; min-width: 284px; margin-left: 16px;"
>
Name
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Type
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
>
Role
</div>
<div
class="AbstractList__header-label"
style="width: 150px;"
/>
</div>
<div
class="AbstractList__body"
>
<div
data-testid="team-row-id1"
>
Team 1
</div>
<div
data-testid="team-row-id2"
>
Team 2
</div>
</div>
<div
class="AbstractList__footer"
>
<div
class="counter"
>
1 - 2 of 2
</div>
<button
class="btn btn-tertiary prev disabled"
disabled=""
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
class="btn btn-tertiary next disabled"
disabled=""
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
</div>
</div>
`;
@@ -1,17 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import AbstractList from './abstract_list';
import TeamRow from './team_row';
import type {TeamWithMembership} from './types';
jest.mock('./team_row', () => {
return function MockTeamRow(props: {team: TeamWithMembership}) {
return <div data-testid={`team-row-${props.team.id}`}>{props.team.display_name}</div>;
};
});
jest.mock('./team_list_dropdown', () => {
return function MockTeamListDropdown() {
return <div data-testid='team-list-dropdown'/>;
};
});
jest.mock('utils/utils', () => ({
imageURLForTeam: jest.fn(() => ''),
localizeMessage: jest.fn((id: string, defaultMessage: string) => defaultMessage),
}));
describe('admin_console/system_user_detail/team_list/AbstractList', () => {
const renderRow = jest.fn((item) => {
const renderRow = jest.fn((item: TeamWithMembership) => {
const MockTeamRow = require('./team_row');
return (
<TeamRow
<MockTeamRow
key={item.id}
team={item}
doRemoveUserFromTeam={jest.fn()}
@@ -21,7 +41,7 @@ describe('admin_console/system_user_detail/team_list/AbstractList', () => {
);
});
const teamsWithMemberships = [
const teamsWithMemberships: TeamWithMembership[] = [
{
id: 'id1',
display_name: 'Team 1',
@@ -73,7 +93,7 @@ describe('admin_console/system_user_detail/team_list/AbstractList', () => {
const defaultProps = {
userId: '1234',
data: [],
data: [] as TeamWithMembership[],
onPageChangedCallback: jest.fn(),
total: 0,
headerLabels,
@@ -83,79 +103,112 @@ describe('admin_console/system_user_detail/team_list/AbstractList', () => {
defaultMessage: 'No teams found',
},
actions: {
getTeamsData: jest.fn().mockResolvedValue(Promise.resolve([])),
getTeamsData: jest.fn().mockResolvedValue([]),
removeGroup: jest.fn(),
},
};
beforeEach(() => {
jest.clearAllMocks();
defaultProps.actions.getTeamsData = jest.fn().mockResolvedValue([]);
});
test('should match snapshot if loading', () => {
const props = defaultProps;
const wrapper = shallow(<AbstractList {...props}/>);
expect(wrapper).toMatchSnapshot();
// Use a never-resolving promise so loading remains true
defaultProps.actions.getTeamsData = jest.fn(() => new Promise(() => {}));
const {container} = renderWithContext(
<AbstractList {...defaultProps}/>,
);
expect(container).toMatchSnapshot();
});
test('should match snapshot no data', () => {
const props = defaultProps;
const wrapper = shallow(<AbstractList {...props}/>);
wrapper.setState({loading: false});
expect(wrapper).toMatchSnapshot();
test('should match snapshot no data', async () => {
const {container} = renderWithContext(
<AbstractList {...defaultProps}/>,
);
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
test('should match snapshot with teams data populated', () => {
const props = defaultProps;
const wrapper = shallow(
test('should match snapshot with teams data populated', async () => {
const {container} = renderWithContext(
<AbstractList
{...props}
{...defaultProps}
data={teamsWithMemberships}
total={2}
/>,
);
wrapper.setState({loading: false});
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with enough teams data to require paging', () => {
const props = defaultProps;
const moreTeams = teamsWithMemberships;
for (let i = 3; i <= 30; i++) {
moreTeams.push({
id: 'id' + i,
display_name: 'Team ' + i,
description: 'Team ' + i + ' description',
} as TeamWithMembership);
}
const wrapper = shallow(
<AbstractList
{...props}
data={moreTeams}
total={30}
/>,
);
wrapper.setState({loading: false});
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot when on second page of pagination', () => {
const props = defaultProps;
const moreTeams = teamsWithMemberships;
for (let i = 3; i <= 30; i++) {
moreTeams.push({
id: 'id' + i,
display_name: 'Team ' + i,
description: 'Team ' + i + ' description',
} as TeamWithMembership);
}
const wrapper = shallow(
<AbstractList
{...props}
data={moreTeams}
total={30}
/>,
);
wrapper.setState({
loading: false,
page: 1,
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot with enough teams data to require paging', async () => {
const moreTeams: TeamWithMembership[] = [...teamsWithMemberships];
for (let i = 3; i <= 30; i++) {
moreTeams.push({
id: 'id' + i,
display_name: 'Team ' + i,
description: 'Team ' + i + ' description',
} as TeamWithMembership);
}
const {container} = renderWithContext(
<AbstractList
{...defaultProps}
data={moreTeams}
total={30}
/>,
);
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
test('should match snapshot when on second page of pagination', async () => {
const moreTeams: TeamWithMembership[] = [...teamsWithMemberships];
for (let i = 3; i <= 30; i++) {
moreTeams.push({
id: 'id' + i,
display_name: 'Team ' + i,
description: 'Team ' + i + ' description',
} as TeamWithMembership);
}
const {container} = renderWithContext(
<AbstractList
{...defaultProps}
data={moreTeams}
total={30}
/>,
);
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
// Click next page button to go to page 1
const nextButton = container.querySelector('button.next');
expect(nextButton).toBeInTheDocument();
await userEvent.click(nextButton!);
// Wait for loading to finish again after page change
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
});
@@ -1,20 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {waitFor} from '@testing-library/react';
import React from 'react';
import {defineMessage} from 'react-intl';
import {renderWithContext} from 'tests/react_testing_utils';
import TeamList from './team_list';
jest.mock('./team_row', () => {
return (props: {team: {id: string; display_name: string}}) => (
<div data-testid={`team-row-${props.team.id}`}>{props.team.display_name}</div>
);
});
jest.mock('components/widgets/team_icon/team_icon', () => {
return () => <div data-testid='team-icon'/>;
});
jest.mock('./team_list_dropdown', () => {
return () => <div data-testid='team-list-dropdown'/>;
});
jest.mock('utils/utils', () => ({
imageURLForTeam: () => '',
}));
describe('admin_console/system_user_detail/team_list/TeamList', () => {
const defaultProps = {
userId: '1234',
locale: 'en',
emptyListTextId: 'emptyListTextId',
emptyListTextDefaultMessage: 'No teams found',
emptyList: defineMessage({
id: 'emptyListTextId',
defaultMessage: 'No teams found',
}),
actions: {
getTeamsData: () => Promise.resolve({data: []}),
getTeamMembersForUser: () => Promise.resolve({data: []}),
getTeamsData: jest.fn().mockResolvedValue({data: []}),
getTeamMembersForUser: jest.fn().mockResolvedValue({data: []}),
removeUserFromTeam: jest.fn(),
updateTeamMemberSchemeRoles: jest.fn(),
},
@@ -22,33 +45,42 @@ describe('admin_console/system_user_detail/team_list/TeamList', () => {
refreshTeams: false,
};
const teamsWithMemberships = [
{
id: 'id1',
display_name: 'Team 1',
description: 'Team 1 description',
},
{
id: 'id2',
display_name: 'Team 2',
description: 'The 2 description',
},
];
test('should match snapshot when no teams are found', async () => {
const {container} = renderWithContext(<TeamList {...defaultProps}/>);
test('should match snapshot when no teams are found', () => {
const props = defaultProps;
const wrapper = shallow(<TeamList {...props}/>);
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
test('should match snapshot with teams populated', () => {
const props = defaultProps;
const wrapper = shallow(
<TeamList
{...props}
/>,
);
wrapper.setState({teamsWithMemberships});
expect(wrapper).toMatchSnapshot();
test('should match snapshot with teams populated', async () => {
const props = {
...defaultProps,
actions: {
...defaultProps.actions,
getTeamsData: jest.fn().mockResolvedValue({
data: [
{id: 'id1', display_name: 'Team 1', description: 'Team 1 description'},
{id: 'id2', display_name: 'Team 2', description: 'The 2 description'},
],
}),
getTeamMembersForUser: jest.fn().mockResolvedValue({
data: [
{team_id: 'id1'},
{team_id: 'id2'},
],
}),
},
};
const {container} = renderWithContext(<TeamList {...props}/>);
await waitFor(() => {
expect(container.querySelector('.AbstractList__loading')).not.toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
});
@@ -1,11 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import Authorize from './authorize';
jest.mock('utils/browser_history', () => ({
getHistory: jest.fn(() => ({replace: jest.fn(), push: jest.fn()})),
}));
describe('components/user_settings/display/UserSettingsDisplay', () => {
const oauthApp = {
id: 'facxd9wpzpbpfp8pad78xj75pr',
@@ -21,42 +28,54 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
callback_urls: ['https://test.com/callback', 'https://test.com/callback2'],
};
const requiredProps = {
location: {search: ''},
let requiredProps: {
location: {search: string};
actions: {
getOAuthAppInfo: jest.fn().mockResolvedValue({data: true}),
allowOAuth2: jest.fn().mockResolvedValue({data: true}),
},
getOAuthAppInfo: jest.Mock;
allowOAuth2: jest.Mock;
};
};
beforeEach(() => {
jest.clearAllMocks();
requiredProps = {
location: {search: ''},
actions: {
getOAuthAppInfo: jest.fn().mockResolvedValue({data: oauthApp}),
allowOAuth2: jest.fn().mockResolvedValue({data: true}),
},
};
});
test('UNSAFE_componentWillMount() should have called getOAuthAppInfo', () => {
const props = {...requiredProps, location: {search: 'client_id=1234abcd'}};
shallow(<Authorize {...props}/>);
renderWithContext(<Authorize {...props}/>);
expect(requiredProps.actions.getOAuthAppInfo).toHaveBeenCalled();
expect(requiredProps.actions.getOAuthAppInfo).toHaveBeenCalledWith('1234abcd');
expect(props.actions.getOAuthAppInfo).toHaveBeenCalled();
expect(props.actions.getOAuthAppInfo).toHaveBeenCalledWith('1234abcd');
});
test('UNSAFE_componentWillMount() should have updated state.app', async () => {
const expected = oauthApp;
const promise = Promise.resolve({data: expected});
const actions = {...requiredProps.actions, getOAuthAppInfo: () => promise};
const props = {...requiredProps, actions, location: {search: 'client_id=1234abcd'}};
const wrapper = shallow<Authorize>(<Authorize {...props}/>);
await promise;
expect(wrapper.state().app).toEqual(expected);
});
test('handleAllow() should have called allowOAuth2', () => {
const props = {...requiredProps, location: {search: 'client_id=1234abcd'}};
const wrapper = shallow<Authorize>(<Authorize {...props}/>);
renderWithContext(<Authorize {...props}/>);
wrapper.instance().handleAllow();
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Allow'})).toBeInTheDocument();
});
});
test('handleAllow() should have called allowOAuth2', async () => {
const props = {...requiredProps, location: {search: 'client_id=1234abcd'}};
renderWithContext(<Authorize {...props}/>);
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Allow'})).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: 'Allow'}));
const expected = {
clientId: '1234abcd',
@@ -68,16 +87,20 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
scope: null,
resource: null,
};
expect(requiredProps.actions.allowOAuth2).toHaveBeenCalled();
expect(requiredProps.actions.allowOAuth2).toHaveBeenCalledWith(expected);
expect(props.actions.allowOAuth2).toHaveBeenCalled();
expect(props.actions.allowOAuth2).toHaveBeenCalledWith(expected);
});
test('handleAllow() should include resource parameter when provided in URL', () => {
test('handleAllow() should include resource parameter when provided in URL', async () => {
const props = {...requiredProps, location: {search: 'client_id=1234abcd&resource=https://example.com/api'}};
const wrapper = shallow<Authorize>(<Authorize {...props}/>);
renderWithContext(<Authorize {...props}/>);
wrapper.instance().handleAllow();
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Allow'})).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: 'Allow'}));
const expected = {
clientId: '1234abcd',
@@ -89,21 +112,28 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
scope: null,
resource: 'https://example.com/api',
};
expect(requiredProps.actions.allowOAuth2).toHaveBeenCalled();
expect(requiredProps.actions.allowOAuth2).toHaveBeenCalledWith(expected);
expect(props.actions.allowOAuth2).toHaveBeenCalled();
expect(props.actions.allowOAuth2).toHaveBeenCalledWith(expected);
});
test('handleAllow() should have updated state.error', async () => {
const error = new Error('error');
const promise = Promise.resolve({error});
const actions = {...requiredProps.actions, allowOAuth2: () => promise};
const actions = {
...requiredProps.actions,
allowOAuth2: jest.fn().mockResolvedValue({error}),
};
const props = {...requiredProps, actions, location: {search: 'client_id=1234abcd'}};
const wrapper = shallow<Authorize>(<Authorize {...props}/>);
renderWithContext(<Authorize {...props}/>);
wrapper.instance().handleAllow();
await promise;
await waitFor(() => {
expect(screen.getByRole('button', {name: 'Allow'})).toBeInTheDocument();
});
expect(wrapper.state().error).toEqual(error.message);
await userEvent.click(screen.getByRole('button', {name: 'Allow'}));
await waitFor(() => {
expect(screen.getByText('error')).toBeInTheDocument();
});
});
});
File diff suppressed because it is too large Load Diff
@@ -1,352 +1,286 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/AbstractOutgoingWebhook should match snapshot 1`] = `
<div
className="backstage-content"
>
<BackstageHeader>
<Link
to="/team_name/integrations/outgoing_webhooks"
>
<MemoizedFormattedMessage
defaultMessage="Outgoing Webhooks"
id="add_outgoing_webhook.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Header"
id="header_id"
/>
</BackstageHeader>
<div>
<div
className="backstage-form"
class="backstage-content"
>
<form
className="form-horizontal"
onSubmit={[Function]}
<div
class="backstage-header"
>
<div
className="form-group"
<h1>
<a
href="/team_name/integrations/outgoing_webhooks"
>
Outgoing Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Header
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<label
className="control-label col-sm-4"
htmlFor="displayName"
>
<MemoizedFormattedMessage
defaultMessage="Title"
id="add_outgoing_webhook.displayName"
/>
</label>
<div
className="col-md-5 col-sm-8"
class="form-group"
>
<input
className="form-control"
id="displayName"
maxLength={64}
onChange={[Function]}
type="text"
value="testOutgoingWebhook"
/>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
className="form__help"
class="col-md-5 col-sm-8"
>
<MemoizedFormattedMessage
defaultMessage="Specify a title, of up to 64 characters, for the webhook settings page."
id="add_outgoing_webhook.displayName.help"
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="testOutgoingWebhook"
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="description"
>
<MemoizedFormattedMessage
defaultMessage="Description"
id="add_outgoing_webhook.description"
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="description"
maxLength={500}
onChange={[Function]}
type="text"
value="testing"
/>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="Describe your outgoing webhook."
id="add_outgoing_webhook.description.help"
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="contentType"
>
<MemoizedFormattedMessage
defaultMessage="Content Type"
id="add_outgoing_webhook.content_Type"
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<select
className="form-control"
onChange={[Function]}
value="test_content_type"
>
<option
value="application/x-www-form-urlencoded"
<div
class="form__help"
>
application/x-www-form-urlencoded
</option>
<option
value="application/json"
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value="testing"
/>
<div
class="form__help"
>
application/json
</option>
</select>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="Specify the content type by which to send the request."
id="add_outgoing_webhook.contentType.help1"
/>
</div>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="For the server to encode the parameters in a URL format in the request body, select application/x-www-form-urlencoded."
id="add_outgoing_webhook.contentType.help2"
/>
</div>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="For the server to format the request body as JSON, select application/json."
id="add_outgoing_webhook.contentType.help3"
/>
Describe your outgoing webhook.
</div>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="channelId"
>
<MemoizedFormattedMessage
defaultMessage="Channel"
id="add_outgoing_webhook.channel"
/>
</label>
<div
className="col-md-5 col-sm-8"
class="form-group"
>
<Connect(Component)
onChange={[Function]}
selectDm={false}
selectOpen={true}
selectPrivate={false}
value="88cxd9wpzpbpfp8pad78xj75pr"
/>
<div
className="form__help"
<label
class="control-label col-sm-4"
for="contentType"
>
<MemoizedFormattedMessage
defaultMessage="This field is optional if you specify at least one trigger word. Specify the public channel that delivers the payload to the webhook."
id="add_outgoing_webhook.channel.help"
/>
Content Type
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
>
<option
value="application/x-www-form-urlencoded"
>
application/x-www-form-urlencoded
</option>
<option
value="application/json"
>
application/json
</option>
</select>
<div
class="form__help"
>
Specify the content type by which to send the request.
</div>
<div
class="form__help"
>
For the server to encode the parameters in a URL format in the request body, select application/x-www-form-urlencoded.
</div>
<div
class="form__help"
>
For the server to format the request body as JSON, select application/json.
</div>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="triggerWords"
>
<MemoizedFormattedMessage
defaultMessage="Trigger Words (One Per Line)"
id="add_outgoing_webhook.triggerWords"
/>
</label>
<div
className="col-md-5 col-sm-8"
class="form-group"
>
<textarea
className="form-control"
id="triggerWords"
maxLength={1000}
onChange={[Function]}
rows={3}
value="test
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="current_channel_id"
>
name
</option>
</select>
<div
class="form__help"
>
This field is optional if you specify at least one trigger word. Specify the public channel that delivers the payload to the webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger Words (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="triggerWords"
maxlength="1000"
rows="3"
>
test
trigger
word
"
/>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="Specify the trigger words that send an HTTP POST request to your application. The trigger can be for the channel, the outgoing webhook, or both. If you select only Channel, trigger words are optional. If you select both, the message must match both values."
id="add_outgoing_webhook.triggerWords.help"
/>
</textarea>
<div
class="form__help"
>
Specify the trigger words that send an HTTP POST request to your application. The trigger can be for the channel, the outgoing webhook, or both. If you select only Channel, trigger words are optional. If you select both, the message must match both values.
</div>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="triggerWords"
>
<MemoizedFormattedMessage
defaultMessage="Trigger When"
id="add_outgoing_webhook.triggerWordsTriggerWhen"
/>
</label>
<div
className="col-md-5 col-sm-8"
class="form-group"
>
<select
className="form-control"
id="triggerWhen"
onChange={[Function]}
value={0}
<label
class="control-label col-sm-4"
for="triggerWords"
>
<option
value="0"
>
First word matches a trigger word exactly
</option>
<option
value="1"
>
First word starts with a trigger word
</option>
</select>
Trigger When
</label>
<div
className="form__help"
class="col-md-5 col-sm-8"
>
<MemoizedFormattedMessage
defaultMessage="Specify when to trigger the outgoing webhook."
id="add_outgoing_webhook.triggerWordsTriggerWhen.help"
/>
<select
class="form-control"
id="triggerWhen"
>
<option
value="0"
>
First word matches a trigger word exactly
</option>
<option
value="1"
>
First word starts with a trigger word
</option>
</select>
<div
class="form__help"
>
Specify when to trigger the outgoing webhook.
</div>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="callbackUrls"
>
<MemoizedFormattedMessage
defaultMessage="Callback URLs (One Per Line)"
id="add_outgoing_webhook.callbackUrls"
/>
</label>
<div
className="col-md-5 col-sm-8"
class="form-group"
>
<textarea
className="form-control"
id="callbackUrls"
maxLength={1000}
onChange={[Function]}
rows={3}
value="callbackUrl1.com
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1000"
rows="3"
>
callbackUrl1.com
callbackUrl2.com
"
/>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="Specify the URL that the messages will be sent to. If the URL is private, add it as a {link}."
id="add_outgoing_webhook.callbackUrls.help"
values={
Object {
"link": <ForwardRef
href="https://mattermost.com/pl/default-allow-untrusted-internal-connections"
location="abstract_outgoing_webhook"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="trusted internal connection"
id="add_outgoing_webhook.callbackUrls.helpLinkText"
/>
</ForwardRef>,
}
}
/>
</textarea>
<div
class="form__help"
>
Specify the URL that the messages will be sent to. If the URL is private, add it as a
<a
href="https://mattermost.com/pl/default-allow-untrusted-internal-connections?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_outgoing_webhook&uid=&sid=&edition=team&server_version="
location="abstract_outgoing_webhook"
rel="noopener noreferrer"
target="_blank"
>
trusted internal connection
</a>
.
</div>
</div>
</div>
</div>
<div
className="backstage-form__footer"
>
<Memo(FormError)
errors={
Array [
"",
null,
]
}
type="backstage"
/>
<Link
className="btn btn-tertiary"
to="/team_name/integrations/outgoing_webhooks"
<div
class="backstage-form__footer"
>
<MemoizedFormattedMessage
defaultMessage="Cancel"
id="add_outgoing_webhook.cancel"
/>
</Link>
<Memo(SpinnerButton)
className="btn btn-primary"
id="saveWebhook"
onClick={[Function]}
spinning={false}
spinningText={
Object {
"defaultMessage": "Loading",
"id": "loading_id",
}
}
type="submit"
>
<MemoizedFormattedMessage
defaultMessage="Footer"
id="footer_id"
/>
</Memo(SpinnerButton)>
</div>
</form>
<a
class="btn btn-tertiary"
href="/team_name/integrations/outgoing_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveWebhook"
type="submit"
>
Footer
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -1,394 +1,294 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/InstalledCommand should call onDelete function 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<div
className="item-details__name"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong>
display_name
</strong>
<div
class="item-details__name"
>
<strong>
display_name
</strong>
</div>
<span
class="item-details__trigger"
>
- /trigger auto_complete_hint
</span>
<div
class="item-actions"
>
<button
class="style--none color--link"
>
Regenerate Token
</button>
-
<a
href="/team_name/integrations/commands/edit?id=r5tpgt4iepf45jt768jz84djic"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<span
className="item-details__trigger"
>
- /trigger auto_complete_hint
</span>
<div
className="item-actions"
class="item-details__row"
>
<button
className="style--none color--link"
onClick={[Function]}
<span
class="item-details__description"
>
<MemoizedFormattedMessage
defaultMessage="Regenerate Token"
id="installed_integrations.regenToken"
/>
</button>
-
<Link
to="/team_name/integrations/commands/edit?id=r5tpgt4iepf45jt768jz84djic"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the slash command and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_commands.delete.confirm"
/>
}
onDelete={[Function]}
/>
description
</span>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
description
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Token:
<code>
testToken
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Token: {token}"
id="installed_integrations.token"
values={
Object {
"token": <code>
testToken
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="testToken"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1499722850203,
"creator": "username",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by username on Monday, July 10, 2017
</span>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledCommand should call onRegenToken function 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<div
className="item-details__name"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong>
display_name
</strong>
<div
class="item-details__name"
>
<strong>
display_name
</strong>
</div>
<span
class="item-details__trigger"
>
- /trigger auto_complete_hint
</span>
<div
class="item-actions"
>
<button
class="style--none color--link"
>
Regenerate Token
</button>
-
<a
href="/team_name/integrations/commands/edit?id=r5tpgt4iepf45jt768jz84djic"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<span
className="item-details__trigger"
>
- /trigger auto_complete_hint
</span>
<div
className="item-actions"
class="item-details__row"
>
<button
className="style--none color--link"
onClick={[Function]}
<span
class="item-details__description"
>
<MemoizedFormattedMessage
defaultMessage="Regenerate Token"
id="installed_integrations.regenToken"
/>
</button>
-
<Link
to="/team_name/integrations/commands/edit?id=r5tpgt4iepf45jt768jz84djic"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the slash command and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_commands.delete.confirm"
/>
}
onDelete={[Function]}
/>
description
</span>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
description
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Token:
<code>
testToken
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Token: {token}"
id="installed_integrations.token"
values={
Object {
"token": <code>
testToken
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="testToken"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1499722850203,
"creator": "username",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by username on Monday, July 10, 2017
</span>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledCommand should filter out command 1`] = `""`;
exports[`components/integrations/InstalledCommand should match snapshot 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<div
className="item-details__name"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong>
display_name
</strong>
<div
class="item-details__name"
>
<strong>
display_name
</strong>
</div>
<span
class="item-details__trigger"
>
- /trigger auto_complete_hint
</span>
</div>
<span
className="item-details__trigger"
<div
class="item-details__row"
>
- /trigger auto_complete_hint
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<span
class="item-details__description"
>
description
</span>
</div>
<div
class="item-details__row"
>
description
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Token:
<code>
testToken
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Token: {token}"
id="installed_integrations.token"
values={
Object {
"token": <code>
testToken
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="testToken"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1499722850203,
"creator": "username",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by username on Monday, July 10, 2017
</span>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledCommand should match snapshot, not autocomplete, no display_name/description/auto_complete_hint 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<div
className="item-details__name"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong>
command_display_name
</strong>
<div
class="item-details__name"
>
<strong>
command_display_name
</strong>
</div>
<span
class="item-details__trigger"
>
- /trigger
</span>
</div>
<span
className="item-details__trigger"
<div
class="item-details__row"
>
- /trigger
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<span
class="item-details__description"
>
command_description
</span>
</div>
<div
class="item-details__row"
>
command_description
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Token:
<code>
testToken
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Token: {token}"
id="installed_integrations.token"
values={
Object {
"token": <code>
testToken
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="testToken"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1499722850203,
"creator": "username",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by username on Monday, July 10, 2017
</span>
</div>
</div>
</div>
</div>
@@ -1,99 +1,74 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/InstalledIncomingWebhook should match snapshot 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<strong
className="item-details__name"
>
build
</strong>
<div
className="item-actions"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<Link
to="/test/integrations/incoming_webhooks/edit?id=9w96t4nhbfdiij64wfqors4i1r"
<strong
class="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the incoming webhook and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_incoming_webhooks.delete.confirm"
/>
}
onDelete={[Function]}
/>
build
</strong>
<div
class="item-actions"
>
<a
href="/test/integrations/incoming_webhooks/edit?id=9w96t4nhbfdiij64wfqors4i1r"
>
Edit
</a>
-
<button
class="color--link style--none"
>
Delete
</button>
</div>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
build status
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="URL: {url}"
id="installed_integrations.url"
values={
Object {
"url": <code>
http://localhost:8065/hooks/9w96t4nhbfdiij64wfqors4i1r
</code>,
}
}
/>
<span>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy URL",
"id": "integrations.copy_url",
}
}
value="http://localhost:8065/hooks/9w96t4nhbfdiij64wfqors4i1r"
/>
<span
class="item-details__description"
>
build status
</span>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1502455422406,
"creator": "creator",
}
}
/>
</span>
<span
class="item-details__url word-break--all"
>
URL:
<code>
http://localhost:8065/hooks/9w96t4nhbfdiij64wfqors4i1r
</code>
<span>
<button
aria-label="Copy URL"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by creator on Friday, August 11, 2017
</span>
</div>
</div>
</div>
</div>
@@ -1,184 +1,137 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/InstalledOutgoingWebhook Should match snapshot of makeDisplayName 1`] = `"hook display name"`;
exports[`components/integrations/InstalledOutgoingWebhook Should match snapshot of makeDisplayName 1`] = `
<strong
class="item-details__name"
>
hook display name
</strong>
`;
exports[`components/integrations/InstalledOutgoingWebhook Should match snapshot of makeDisplayName 2`] = `"channel display name"`;
exports[`components/integrations/InstalledOutgoingWebhook Should match snapshot of makeDisplayName 2`] = `
<strong
class="item-details__name"
>
channel display name
</strong>
`;
exports[`components/integrations/InstalledOutgoingWebhook Should match snapshot of makeDisplayName 3`] = `"name"`;
exports[`components/integrations/InstalledOutgoingWebhook Should match snapshot of makeDisplayName 3`] = `
<strong
class="item-details__name"
>
A Private Webhook
</strong>
`;
exports[`components/integrations/InstalledOutgoingWebhook should match snapshot 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<strong
className="item-details__name"
>
build
</strong>
<div
className="item-actions"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<button
className="style--none color--link"
onClick={[Function]}
<strong
class="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Regenerate Token"
id="installed_integrations.regenToken"
/>
</button>
-
<Link
to="/test/integrations/outgoing_webhooks/edit?id=7h88x419ubbyuxzs7dfwtgkffr"
build
</strong>
<div
class="item-actions"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the outgoing webhook and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_outgoing_webhooks.delete.confirm"
/>
}
onDelete={[Function]}
/>
<button
class="style--none color--link"
>
Regenerate Token
</button>
-
<a
href="/test/integrations/outgoing_webhooks/edit?id=7h88x419ubbyuxzs7dfwtgkffr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
build status
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__content_type"
<span
class="item-details__description"
>
build status
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Content-Type: {contentType}"
id="installed_integrations.content_type"
values={
Object {
"contentType": "application/x-www-form-urlencoded",
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__trigger-words"
<span
class="item-details__content_type"
>
Content-Type: application/x-www-form-urlencoded
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Trigger Words: {triggerWords}"
id="installed_integrations.triggerWords"
values={
Object {
"triggerWords": "build",
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__trigger-when"
<span
class="item-details__trigger-words"
>
Trigger Words: build
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Trigger When: {triggerWhen}"
id="installed_integrations.triggerWhen"
values={
Object {
"triggerWhen": <Memo(MemoizedFormattedMessage)
defaultMessage="First word matches a trigger word exactly"
id="add_outgoing_webhook.triggerWordsTriggerWhenFullWord"
/>,
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__trigger-when"
>
Trigger When:
First word matches a trigger word exactly
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Token: {token}"
id="installed_integrations.token"
values={
Object {
"token": <code>
xoxz1z7c3tgi9xhrfudn638q9r
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="xoxz1z7c3tgi9xhrfudn638q9r"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
<span
class="item-details__token"
>
Token:
<code>
xoxz1z7c3tgi9xhrfudn638q9r
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1508327769020,
"creator": "some-user",
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__creation"
>
Created by some-user on Wednesday, October 18, 2017
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Callback URLs: {urls}"
id="installed_integrations.callback_urls"
values={
Object {
"urls": "http://adsfdasd.com",
}
}
/>
</span>
<span
class="item-details__url word-break--all"
>
Callback URLs: http://adsfdasd.com
</span>
</div>
</div>
</div>
</div>
@@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {FormEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import AbstractCommand from 'components/integrations/abstract_command';
import {renderWithContext, screen, userEvent, fireEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
describe('components/integrations/AbstractCommand', () => {
@@ -56,14 +54,14 @@ describe('components/integrations/AbstractCommand', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AbstractCommand {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when header/footer/loading is a string', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AbstractCommand
{...baseProps}
header='Header as string'
@@ -71,118 +69,26 @@ describe('components/integrations/AbstractCommand', () => {
footer={'Footer as string'}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, displays client error', () => {
test('should match snapshot, displays client error', async () => {
const newSeverError = 'server error';
const props = {...baseProps, serverError: newSeverError};
const wrapper = shallow(
const {container} = renderWithContext(
<AbstractCommand {...props}/>,
);
wrapper.find('#trigger').simulate('change', {target: {value: ''}});
wrapper.find('.btn-primary').simulate('click', {preventDefault: jest.fn()});
const triggerInput = container.querySelector('#trigger') as HTMLInputElement;
await userEvent.clear(triggerInput);
expect(wrapper).toMatchSnapshot();
await userEvent.click(screen.getByText('Footer'));
expect(container).toMatchSnapshot();
expect(action).not.toHaveBeenCalled();
});
test('should call action function', () => {
const wrapper = shallow(
<AbstractCommand {...baseProps}/>,
);
wrapper.find('#displayName').simulate('change', {target: {value: 'name'}});
wrapper.find('.btn-primary').simulate('click', {preventDefault: jest.fn()});
expect(action).toHaveBeenCalled();
});
test('should match object returned by getStateFromCommand', () => {
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...baseProps}/>,
);
const instance = wrapper.instance();
const expectedOutput = {
autocomplete: true,
autocompleteDescription: 'auto_complete_desc',
autocompleteHint: 'auto_complete_hint',
clientError: null,
description: 'description',
displayName: 'display_name',
iconUrl: 'https://google.com/icon',
method: 'G',
saving: false,
trigger: 'trigger',
url: 'https://google.com/command',
username: 'username',
};
expect(instance.getStateFromCommand(command)).toEqual(expectedOutput);
});
test('should match state when method is called', () => {
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...baseProps}/>,
);
const instance = wrapper.instance();
const displayName = 'new display_name';
const displayNameEvent = {preventDefault: jest.fn(), target: {value: displayName}} as any;
instance.updateDisplayName(displayNameEvent);
expect(wrapper.state('displayName')).toEqual(displayName);
const description = 'new description';
const descriptionEvent = {preventDefault: jest.fn(), target: {value: description}} as any;
instance.updateDescription(descriptionEvent);
expect(wrapper.state('description')).toEqual(description);
const trigger = 'new trigger';
const triggerEvent = {preventDefault: jest.fn(), target: {value: trigger}} as any;
instance.updateTrigger(triggerEvent);
expect(wrapper.state('trigger')).toEqual(trigger);
const url = 'new url';
const urlEvent = {preventDefault: jest.fn(), target: {value: url}} as any;
instance.updateUrl(urlEvent);
expect(wrapper.state('url')).toEqual(url);
const method = 'P';
const methodEvent = {preventDefault: jest.fn(), target: {value: method}} as any;
instance.updateMethod(methodEvent);
expect(wrapper.state('method')).toEqual(method);
const username = 'new username';
const usernameEvent = {preventDefault: jest.fn(), target: {value: username}} as any;
instance.updateUsername(usernameEvent);
expect(wrapper.state('username')).toEqual(username);
const iconUrl = 'new iconUrl';
const iconUrlEvent = {preventDefault: jest.fn(), target: {value: iconUrl}} as any;
instance.updateIconUrl(iconUrlEvent);
expect(wrapper.state('iconUrl')).toEqual(iconUrl);
const trueUpdateAutocompleteEvent = {target: {checked: true}} as any;
const falseeUpdateAutocompleteEvent = {target: {checked: false}} as any;
instance.updateAutocomplete(trueUpdateAutocompleteEvent);
expect(wrapper.state('autocomplete')).toEqual(true);
instance.updateAutocomplete(falseeUpdateAutocompleteEvent);
expect(wrapper.state('autocomplete')).toEqual(false);
const autocompleteHint = 'new autocompleteHint';
const autocompleteHintEvent = {preventDefault: jest.fn(), target: {value: autocompleteHint}} as any;
instance.updateAutocompleteHint(autocompleteHintEvent);
expect(wrapper.state('autocompleteHint')).toEqual(autocompleteHint);
const autocompleteDescription = 'new autocompleteDescription';
const autocompleteDescriptionEvent = {preventDefault: jest.fn(), target: {value: autocompleteDescription}} as any;
instance.updateAutocompleteDescription(autocompleteDescriptionEvent);
expect(wrapper.state('autocompleteDescription')).toEqual(autocompleteDescription);
});
test('should match state when handleSubmit is called', () => {
test('should call action function', async () => {
const newAction = jest.fn().mockImplementation(
() => {
return new Promise<void>((resolve) => {
@@ -191,95 +97,171 @@ describe('components/integrations/AbstractCommand', () => {
},
);
const props = {...baseProps, action: newAction};
const wrapper = shallow<AbstractCommand>(
renderWithContext(
<AbstractCommand {...props}/>,
);
const instance = wrapper.instance();
const displayNameInput = document.querySelector('#displayName') as HTMLInputElement;
await userEvent.clear(displayNameInput);
await userEvent.type(displayNameInput, 'name');
await userEvent.click(screen.getByText('Footer'));
expect(newAction).toHaveBeenCalled();
});
test('should match object returned by getStateFromCommand', () => {
const {container} = renderWithContext(
<AbstractCommand {...baseProps}/>,
);
// Verify the form fields are populated correctly from the initial command
expect((container.querySelector('#displayName') as HTMLInputElement).value).toBe('display_name');
expect((container.querySelector('#description') as HTMLInputElement).value).toBe('description');
expect((container.querySelector('#trigger') as HTMLInputElement).value).toBe('trigger');
expect((container.querySelector('#url') as HTMLInputElement).value).toBe('https://google.com/command');
expect((container.querySelector('#method') as HTMLSelectElement).value).toBe('G');
expect((container.querySelector('#username') as HTMLInputElement).value).toBe('username');
expect((container.querySelector('#iconUrl') as HTMLInputElement).value).toBe('https://google.com/icon');
expect((container.querySelector('#autocomplete') as HTMLInputElement).checked).toBe(true);
expect((container.querySelector('#autocompleteHint') as HTMLInputElement).value).toBe('auto_complete_hint');
});
test('should match state when method is called', async () => {
const {container} = renderWithContext(
<AbstractCommand {...baseProps}/>,
);
const displayNameInput = container.querySelector('#displayName') as HTMLInputElement;
await userEvent.clear(displayNameInput);
await userEvent.type(displayNameInput, 'new display_name');
expect(displayNameInput.value).toBe('new display_name');
const descriptionInput = container.querySelector('#description') as HTMLInputElement;
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, 'new description');
expect(descriptionInput.value).toBe('new description');
const triggerInput = container.querySelector('#trigger') as HTMLInputElement;
await userEvent.clear(triggerInput);
await userEvent.type(triggerInput, 'new trigger');
expect(triggerInput.value).toBe('new trigger');
const urlInput = container.querySelector('#url') as HTMLInputElement;
await userEvent.clear(urlInput);
await userEvent.type(urlInput, 'new url');
expect(urlInput.value).toBe('new url');
const methodSelect = container.querySelector('#method') as HTMLSelectElement;
await userEvent.selectOptions(methodSelect, 'P');
expect(methodSelect.value).toBe('P');
const usernameInput = container.querySelector('#username') as HTMLInputElement;
await userEvent.clear(usernameInput);
await userEvent.type(usernameInput, 'new username');
expect(usernameInput.value).toBe('new username');
const iconUrlInput = container.querySelector('#iconUrl') as HTMLInputElement;
await userEvent.clear(iconUrlInput);
await userEvent.type(iconUrlInput, 'new iconUrl');
expect(iconUrlInput.value).toBe('new iconUrl');
const autocompleteCheckbox = container.querySelector('#autocomplete') as HTMLInputElement;
// autocomplete is initially true, uncheck it
await userEvent.click(autocompleteCheckbox);
expect(autocompleteCheckbox.checked).toBe(false);
// check it again
await userEvent.click(autocompleteCheckbox);
expect(autocompleteCheckbox.checked).toBe(true);
const autocompleteHintInput = container.querySelector('#autocompleteHint') as HTMLInputElement;
await userEvent.clear(autocompleteHintInput);
await userEvent.type(autocompleteHintInput, 'new autocompleteHint');
expect(autocompleteHintInput.value).toBe('new autocompleteHint');
});
test('should match state when handleSubmit is called', async () => {
const newAction = jest.fn().mockImplementation(
() => {
return new Promise<void>((resolve) => {
process.nextTick(() => resolve());
});
},
);
const props = {...baseProps, action: newAction};
const {container} = renderWithContext(
<AbstractCommand {...props}/>,
);
const triggerInput = container.querySelector('#trigger') as HTMLInputElement;
const urlInput = container.querySelector('#url') as HTMLInputElement;
const submitButton = screen.getByText('Footer');
expect(newAction).toHaveBeenCalledTimes(0);
const evt = {preventDefault: jest.fn()} as unknown as FormEvent<Element>;
const handleSubmit = instance.handleSubmit;
handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(true);
expect(wrapper.state('clientError')).toEqual('');
expect(newAction).toHaveBeenCalledTimes(1);
// saving is true
wrapper.setState({saving: true});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual('');
expect(newAction).toHaveBeenCalledTimes(1);
// valid submit
await userEvent.click(submitButton);
await waitFor(() => {
expect(newAction).toHaveBeenCalledTimes(1);
});
// empty trigger
wrapper.setState({saving: false, trigger: ''});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='A trigger word is required'
id='add_command.triggerRequired'
/>,
);
await userEvent.clear(triggerInput);
await userEvent.click(submitButton);
expect(screen.getByText('A trigger word is required')).toBeInTheDocument();
expect(newAction).toHaveBeenCalledTimes(1);
// trigger that starts with a slash '/'
wrapper.setState({saving: false, trigger: '//startwithslash'});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='A trigger word cannot begin with a /'
id='add_command.triggerInvalidSlash'
/>,
);
// trigger that starts with a slash '/' (after trim/lowercase, leading slash is stripped, leaving '/startwithslash' which starts with /)
await userEvent.clear(triggerInput);
await userEvent.type(triggerInput, '//startwithslash');
await userEvent.click(submitButton);
expect(screen.getByText('A trigger word cannot begin with a /')).toBeInTheDocument();
expect(newAction).toHaveBeenCalledTimes(1);
// trigger with space
wrapper.setState({saving: false, trigger: '/trigger with space'});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='A trigger word must not contain spaces'
id='add_command.triggerInvalidSpace'
/>,
);
await userEvent.clear(triggerInput);
await userEvent.type(triggerInput, '/trigger with space');
await userEvent.click(submitButton);
expect(screen.getByText('A trigger word must not contain spaces')).toBeInTheDocument();
expect(newAction).toHaveBeenCalledTimes(1);
// trigger above maximum length
wrapper.setState({saving: false, trigger: '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='A trigger word must contain between {min} and {max} characters'
id='add_command.triggerInvalidLength'
values={{max: 128, min: 1}}
/>,
);
// trigger above maximum length - use fireEvent.change to bypass maxLength constraint
fireEvent.change(triggerInput, {target: {value: '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'}});
await userEvent.click(submitButton);
expect(screen.getByText(/A trigger word must contain between/)).toBeInTheDocument();
expect(newAction).toHaveBeenCalledTimes(1);
// good triggers
wrapper.setState({saving: false, trigger: '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual('');
expect(newAction).toHaveBeenCalledTimes(2);
// good triggers - 128 chars
fireEvent.change(triggerInput, {target: {value: '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'}});
await userEvent.click(submitButton);
await waitFor(() => {
expect(newAction).toHaveBeenCalledTimes(2);
});
wrapper.setState({saving: false, trigger: '/trigger'});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual('');
expect(newAction).toHaveBeenCalledTimes(3);
// good trigger with leading slash
await userEvent.clear(triggerInput);
await userEvent.type(triggerInput, '/trigger');
await userEvent.click(submitButton);
await waitFor(() => {
expect(newAction).toHaveBeenCalledTimes(3);
});
wrapper.setState({saving: false, trigger: 'trigger'});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual('');
expect(newAction).toHaveBeenCalledTimes(4);
// good trigger without slash
await userEvent.clear(triggerInput);
await userEvent.type(triggerInput, 'trigger');
await userEvent.click(submitButton);
await waitFor(() => {
expect(newAction).toHaveBeenCalledTimes(4);
});
// empty url
wrapper.setState({saving: false, trigger: 'trigger', url: ''});
handleSubmit(evt);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='A request URL is required'
id='add_command.urlRequired'
/>,
);
await userEvent.clear(urlInput);
await userEvent.clear(triggerInput);
await userEvent.type(triggerInput, 'trigger');
await userEvent.click(submitButton);
expect(screen.getByText('A request URL is required')).toBeInTheDocument();
expect(newAction).toHaveBeenCalledTimes(4);
});
});
@@ -1,14 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {DeepPartial} from '@mattermost/types/utilities';
import ChannelSelect from 'components/channel_select';
import AbstractIncomingWebhook from 'components/integrations/abstract_incoming_webhook';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
type AbstractIncomingWebhookProps = React.ComponentProps<typeof AbstractIncomingWebhook>;
describe('components/integrations/AbstractIncomingWebhook', () => {
@@ -34,10 +39,10 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
const serverError = '';
const initialHook = {
display_name: 'testIncomingWebhook',
channel_id: '88cxd9wpzpbpfp8pad78xj75pr',
channel_id: 'current_channel_id',
description: 'testing',
id: 'test_id',
team_id: 'test_team_id',
team_id: 'team_id',
create_at: 0,
update_at: 0,
delete_at: 0,
@@ -70,28 +75,92 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
canBypassChannelLock: true,
};
const initialState: DeepPartial<GlobalState> = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: TestHelper.getChannelMock({
id: 'current_channel_id',
team_id: 'team_id',
type: 'O' as ChannelType,
name: 'current_channel',
display_name: 'Current Channel',
}),
},
myMembers: {
current_channel_id: TestHelper.getChannelMembershipMock({channel_id: 'current_channel_id'}),
},
channelsInTeam: {
team_id: new Set(['current_channel_id']),
},
},
teams: {
currentTeamId: 'team_id',
teams: {
team_id: TestHelper.getTeamMock({id: 'team_id'}),
},
myMembers: {
team_id: TestHelper.getTeamMembershipMock({roles: 'team_roles'}),
},
},
},
};
/** Two open team channels the user belongs to; used to assert channel select updates real state. */
const stateWithTwoTeamChannels: DeepPartial<GlobalState> = {
...initialState,
entities: {
...initialState.entities,
channels: {
...initialState.entities?.channels,
channels: {
...initialState.entities?.channels?.channels,
other_channel_id: TestHelper.getChannelMock({
id: 'other_channel_id',
team_id: 'team_id',
type: 'O' as ChannelType,
name: 'other_channel',
display_name: 'Other Channel',
}),
},
myMembers: {
...initialState.entities?.channels?.myMembers,
other_channel_id: TestHelper.getChannelMembershipMock({channel_id: 'other_channel_id'}),
},
channelsInTeam: {
team_id: new Set(['current_channel_id', 'other_channel_id']),
},
},
teams: initialState.entities?.teams,
},
};
beforeEach(() => {
action.mockClear();
});
test('should match snapshot', () => {
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AbstractIncomingWebhook {...requiredProps}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
test('should match snapshot, on serverError', () => {
const newServerError = 'serverError';
const props = {...requiredProps, serverError: newServerError};
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AbstractIncomingWebhook {...props}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
test('should match snapshot, displays client error when no initial hook', () => {
test('should match snapshot, displays client error when no initial hook', async () => {
const props = {...requiredProps};
delete props.initialHook;
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
const {container} = renderWithContext(<AbstractIncomingWebhook {...props}/>, initialState as GlobalState);
await userEvent.click(screen.getByRole('button', {name: footer.defaultMessage}));
expect(action).not.toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, hiding post username if not enabled', () => {
@@ -99,8 +168,8 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
...requiredProps,
enablePostUsernameOverride: false,
};
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AbstractIncomingWebhook {...props}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
test('should match snapshot, hiding post icon url if not enabled', () => {
@@ -108,78 +177,78 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
...requiredProps,
enablePostIconOverride: false,
};
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AbstractIncomingWebhook {...props}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
test('should call action function', () => {
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
test('should call action function', async () => {
const {container} = renderWithContext(<AbstractIncomingWebhook {...requiredProps}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
wrapper.find('#displayName').simulate('change', {target: {value: 'name'}});
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
await userEvent.type(screen.getByRole('textbox', {name: 'Title'}), 'name');
await userEvent.click(screen.getByRole('button', {name: footer.defaultMessage}));
expect(action).toHaveBeenCalled();
expect(action).toHaveBeenCalledTimes(1);
});
test('should update state.channelId when on channel change', () => {
const newChannelId = 'new_channel_id';
const evt = {
preventDefault: jest.fn(),
target: {value: newChannelId},
test('should update state.channelId when on channel change', async () => {
const propsWithInitialOtherChannel = {
...requiredProps,
initialHook: {
...initialHook,
channel_id: 'other_channel_id',
},
};
renderWithContext(
<AbstractIncomingWebhook {...propsWithInitialOtherChannel}/>,
stateWithTwoTeamChannels as GlobalState,
);
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find(ChannelSelect).simulate('change', evt);
const channelSelect = screen.getByRole<HTMLSelectElement>('combobox');
expect(channelSelect.value).toBe('other_channel_id');
expect(wrapper.state('channelId')).toBe(newChannelId);
await userEvent.selectOptions(channelSelect, 'current_channel_id');
expect(channelSelect.value).toBe('current_channel_id');
});
test('should update state.description when on description change', () => {
test('should update state.description when on description change', async () => {
const newDescription = 'new_description';
const evt = {
preventDefault: jest.fn(),
target: {value: newDescription},
};
renderWithContext(<AbstractIncomingWebhook {...requiredProps}/>, initialState as GlobalState);
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find('#description').simulate('change', evt);
const descriptionInput = screen.getByRole('textbox', {name: 'Description'});
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, newDescription);
expect(wrapper.state('description')).toBe(newDescription);
expect(descriptionInput).toHaveValue(newDescription);
});
test('should update state.username on post username change', () => {
test('should update state.username on post username change', async () => {
const newUsername = 'new_username';
const evt = {
preventDefault: jest.fn(),
target: {value: newUsername},
};
renderWithContext(<AbstractIncomingWebhook {...requiredProps}/>, initialState as GlobalState);
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find('#username').simulate('change', evt);
const usernameInput = screen.getByRole('textbox', {name: 'Username'});
await userEvent.clear(usernameInput);
await userEvent.type(usernameInput, newUsername);
expect(wrapper.state('username')).toBe(newUsername);
expect(usernameInput).toHaveValue(newUsername);
});
test('should update state.iconURL on post icon url change', () => {
test('should update state.iconURL on post icon url change', async () => {
const newIconURL = 'http://example.com/icon';
const evt = {
preventDefault: jest.fn(),
target: {value: newIconURL},
};
renderWithContext(<AbstractIncomingWebhook {...requiredProps}/>, initialState as GlobalState);
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find('#iconURL').simulate('change', evt);
const iconInput = screen.getByRole('textbox', {name: 'Profile Picture'});
await userEvent.clear(iconInput);
await userEvent.type(iconInput, newIconURL);
expect(wrapper.state('iconURL')).toBe(newIconURL);
expect(iconInput).toHaveValue(newIconURL);
});
test('should match snapshot when channelLocked is true', () => {
const props = {...requiredProps, channelLocked: true};
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AbstractIncomingWebhook {...props}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
});
@@ -1,14 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React, {type ChangeEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import React from 'react';
import AbstractOAuthApp from 'components/integrations/abstract_oauth_app';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/permissions_gates/system_permission_gate', () => ({children}: {children: React.ReactNode}) => <>{children}</>);
describe('components/integrations/AbstractOAuthApp', () => {
const header = {id: 'Header', defaultMessage: 'Header'};
const footer = {id: 'Footer', defaultMessage: 'Footer'};
@@ -37,6 +38,10 @@ describe('components/integrations/AbstractOAuthApp', () => {
const team = TestHelper.getTeamMock({name: 'test', id: initialApp.id});
beforeEach(() => {
action.mockClear();
});
const baseProps = {
team,
header,
@@ -49,218 +54,209 @@ describe('components/integrations/AbstractOAuthApp', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AbstractOAuthApp {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, displays client error', () => {
test('should match snapshot, displays client error', async () => {
const newServerError = 'serverError';
const props = {...baseProps, serverError: newServerError};
const wrapper = shallow(
const {container} = renderWithContext(
<AbstractOAuthApp {...props}/>,
);
wrapper.find('#callbackUrls').simulate('change', {target: {value: ''}});
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
const callbackUrlsInput = screen.getByRole('textbox', {name: 'Callback URLs (One Per Line)'});
await userEvent.clear(callbackUrlsInput);
expect(action).not.toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
const submitButton = screen.getByRole('button', {name: 'Footer'});
await userEvent.click(submitButton);
expect(props.action).not.toHaveBeenCalled();
expect(container).toMatchSnapshot();
});
test('should call action function', () => {
test('should call action function', async () => {
const props = {...baseProps, action};
const wrapper = shallow(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
wrapper.find('#name').simulate('change', {target: {value: 'name'}});
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
const nameInput = screen.getByRole('textbox', {name: 'Display Name'});
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'name');
const submitButton = screen.getByRole('button', {name: 'Footer'});
await userEvent.click(submitButton);
expect(action).toHaveBeenCalled();
});
test('should have correct state when updateName is called', () => {
test('should have correct state when updateName is called', async () => {
const props = {...baseProps, action};
const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
const evt = {preventDefault: jest.fn(), target: {value: 'new name'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateName(evt);
expect(wrapper.state('name')).toEqual('new name');
const evt2 = {preventDefault: jest.fn(), target: {value: 'another name'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateName(evt2);
expect(wrapper.state('name')).toEqual('another name');
});
test('should have correct state when updateTrusted is called', () => {
const props = {...baseProps, action};
const wrapper = shallow<AbstractOAuthApp>(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const evt = {preventDefault: jest.fn(), target: {value: 'false'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateTrusted(evt);
expect(wrapper.state('is_trusted')).toEqual(false);
const nameInput = screen.getByRole('textbox', {name: 'Display Name'});
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'new name');
expect(nameInput).toHaveValue('new name');
const evt2 = {preventDefault: jest.fn(), target: {value: 'true'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateTrusted(evt2);
expect(wrapper.state('is_trusted')).toEqual(true);
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'another name');
expect(nameInput).toHaveValue('another name');
});
test('should have correct state when updateDescription is called', () => {
test('should have correct state when updateTrusted is called', async () => {
const props = {...baseProps, action};
const wrapper = shallow<AbstractOAuthApp>(
const {container} = renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const evt = {preventDefault: jest.fn(), target: {value: 'new description'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateDescription(evt);
expect(wrapper.state('description')).toEqual('new description');
// Use name attribute to target is_trusted radio buttons specifically
const trustedNoRadio = container.querySelector('input[name="is_trusted"][value="false"]') as HTMLInputElement;
const trustedYesRadio = container.querySelector('input[name="is_trusted"][value="true"]') as HTMLInputElement;
const evt2 = {preventDefault: jest.fn(), target: {value: 'another description'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateDescription(evt2);
expect(wrapper.state('description')).toEqual('another description');
await userEvent.click(trustedNoRadio);
expect(trustedNoRadio).toBeChecked();
await userEvent.click(trustedYesRadio);
expect(trustedYesRadio).toBeChecked();
});
test('should have correct state when updateHomepage is called', () => {
test('should have correct state when updateDescription is called', async () => {
const props = {...baseProps, action};
const wrapper = shallow<AbstractOAuthApp>(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const evt = {preventDefault: jest.fn(), target: {value: 'new homepage'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateHomepage(evt);
expect(wrapper.state('homepage')).toEqual('new homepage');
const descriptionInput = screen.getByRole('textbox', {name: 'Description'});
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, 'new description');
expect(descriptionInput).toHaveValue('new description');
const evt2 = {preventDefault: jest.fn(), target: {value: 'another homepage'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateHomepage(evt2);
expect(wrapper.state('homepage')).toEqual('another homepage');
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, 'another description');
expect(descriptionInput).toHaveValue('another description');
});
test('should have correct state when updateIconUrl is called', () => {
test('should have correct state when updateHomepage is called', async () => {
const props = {...baseProps, action};
const wrapper = shallow<AbstractOAuthApp>(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
wrapper.setState({has_icon: true});
const evt = {preventDefault: jest.fn(), target: {value: 'https://test.com/new_icon_url'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateIconUrl(evt);
expect(wrapper.state('icon_url')).toEqual('https://test.com/new_icon_url');
expect(wrapper.state('has_icon')).toEqual(false);
const homepageInput = screen.getByRole('textbox', {name: 'Homepage'});
await userEvent.clear(homepageInput);
await userEvent.type(homepageInput, 'new homepage');
expect(homepageInput).toHaveValue('new homepage');
wrapper.setState({has_icon: true});
const evt2 = {preventDefault: jest.fn(), target: {value: 'https://test.com/another_icon_url'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateIconUrl(evt2);
expect(wrapper.state('icon_url')).toEqual('https://test.com/another_icon_url');
expect(wrapper.state('has_icon')).toEqual(false);
await userEvent.clear(homepageInput);
await userEvent.type(homepageInput, 'another homepage');
expect(homepageInput).toHaveValue('another homepage');
});
test('should have correct state when handleSubmit is called', () => {
test('should have correct state when updateIconUrl is called', async () => {
const props = {...baseProps, action};
const wrapper = shallow<AbstractOAuthApp>(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const newState = {saving: false, name: 'name', description: 'description', homepage: 'homepage'};
const evt = {preventDefault: jest.fn()} as any;
wrapper.setState({saving: true});
wrapper.instance().handleSubmit(evt);
expect(evt.preventDefault).toHaveBeenCalled();
const iconUrlInput = screen.getByRole('textbox', {name: 'Icon URL'});
await userEvent.clear(iconUrlInput);
await userEvent.type(iconUrlInput, 'https://test.com/new_icon_url');
expect(iconUrlInput).toHaveValue('https://test.com/new_icon_url');
wrapper.setState(newState);
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(true);
expect(wrapper.state('clientError')).toEqual('');
wrapper.setState({...newState, name: ''});
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(false);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='Name for the OAuth 2.0 application is required.'
id='add_oauth_app.nameRequired'
/>,
);
wrapper.setState({...newState, description: ''});
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(false);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='Description for the OAuth 2.0 application is required.'
id='add_oauth_app.descriptionRequired'
/>,
);
wrapper.setState({...newState, homepage: ''});
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(false);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='Homepage for the OAuth 2.0 application is required.'
id='add_oauth_app.homepageRequired'
/>,
);
await userEvent.clear(iconUrlInput);
await userEvent.type(iconUrlInput, 'https://test.com/another_icon_url');
expect(iconUrlInput).toHaveValue('https://test.com/another_icon_url');
});
test('should not require description and homepage for dynamically registered apps', () => {
test('should have correct state when handleSubmit is called', async () => {
const props = {...baseProps, action};
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const nameInput = screen.getByRole('textbox', {name: 'Display Name'});
const descriptionInput = screen.getByRole('textbox', {name: 'Description'});
const homepageInput = screen.getByRole('textbox', {name: 'Homepage'});
const submitButton = screen.getByRole('button', {name: 'Footer'});
// Clear name to trigger name required error
await userEvent.clear(nameInput);
await userEvent.click(submitButton);
expect(screen.getByText('Name for the OAuth 2.0 application is required.')).toBeInTheDocument();
// Set name, clear description to trigger description required error
await userEvent.type(nameInput, 'name');
await userEvent.clear(descriptionInput);
await userEvent.click(submitButton);
expect(screen.getByText('Description for the OAuth 2.0 application is required.')).toBeInTheDocument();
// Set description, clear homepage to trigger homepage required error
await userEvent.type(descriptionInput, 'description');
await userEvent.clear(homepageInput);
await userEvent.click(submitButton);
expect(screen.getByText('Homepage for the OAuth 2.0 application is required.')).toBeInTheDocument();
});
test('should not require description and homepage for dynamically registered apps', async () => {
const dynamicApp = {
...initialApp,
is_dynamically_registered: true,
};
const props = {...baseProps, action, initialApp: dynamicApp};
const wrapper = shallow<AbstractOAuthApp>(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const newState = {saving: false, name: 'name', description: '', homepage: '', callbackUrls: 'https://test.com/callback'};
const evt = {preventDefault: jest.fn()} as any;
const nameInput = screen.getByRole('textbox', {name: 'Display Name'});
const descriptionInput = screen.getByRole('textbox', {name: 'Description'});
const homepageInput = screen.getByRole('textbox', {name: 'Homepage'});
const submitButton = screen.getByRole('button', {name: 'Footer'});
wrapper.setState(newState);
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(true);
expect(wrapper.state('clientError')).toEqual('');
expect(action).toHaveBeenCalled();
// Set name, clear description and homepage
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'name');
await userEvent.clear(descriptionInput);
await userEvent.clear(homepageInput);
await userEvent.click(submitButton);
await waitFor(() => {
expect(action).toHaveBeenCalledTimes(1);
});
});
test('should still require description and homepage for regular OAuth apps', () => {
test('should still require description and homepage for regular OAuth apps', async () => {
const regularApp = {
...initialApp,
is_dynamically_registered: false,
};
const props = {...baseProps, action, initialApp: regularApp};
const wrapper = shallow<AbstractOAuthApp>(
renderWithContext(
<AbstractOAuthApp {...props}/>,
);
const newState = {saving: false, name: 'name', description: '', homepage: '', callbackUrls: 'https://test.com/callback'};
const evt = {preventDefault: jest.fn()} as any;
const nameInput = screen.getByRole('textbox', {name: 'Display Name'});
const descriptionInput = screen.getByRole('textbox', {name: 'Description'});
const homepageInput = screen.getByRole('textbox', {name: 'Homepage'});
const submitButton = screen.getByRole('button', {name: 'Footer'});
wrapper.setState({...newState, description: ''});
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(false);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='Description for the OAuth 2.0 application is required.'
id='add_oauth_app.descriptionRequired'
/>,
);
// Set name, clear description
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'name');
await userEvent.clear(descriptionInput);
await userEvent.click(submitButton);
expect(screen.getByText('Description for the OAuth 2.0 application is required.')).toBeInTheDocument();
wrapper.setState({...newState, description: 'test', homepage: ''});
wrapper.instance().handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(false);
expect(wrapper.state('clientError')).toEqual(
<FormattedMessage
defaultMessage='Homepage for the OAuth 2.0 application is required.'
id='add_oauth_app.homepageRequired'
/>,
);
// Set description, clear homepage
await userEvent.type(descriptionInput, 'test');
await userEvent.clear(homepageInput);
await userEvent.click(submitButton);
expect(screen.getByText('Homepage for the OAuth 2.0 application is required.')).toBeInTheDocument();
});
});
@@ -1,14 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
import type {DeepPartial} from '@mattermost/types/utilities';
import ChannelSelect from 'components/channel_select';
import AbstractOutgoingWebhook from 'components/integrations/abstract_outgoing_webhook';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
const initialState: DeepPartial<GlobalState> = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: TestHelper.getChannelMock({
id: 'current_channel_id',
team_id: 'team_id',
type: 'O' as ChannelType,
name: 'current_channel',
}),
},
myMembers: {
current_channel_id: TestHelper.getChannelMembershipMock({channel_id: 'current_channel_id'}),
},
channelsInTeam: {
team_id: new Set(['current_channel_id']),
},
},
teams: {
currentTeamId: 'team_id',
teams: {
team_id: TestHelper.getTeamMock({id: 'team_id'}),
},
myMembers: {
team_id: TestHelper.getTeamMembershipMock({roles: 'team_roles'}),
},
},
},
};
describe('components/integrations/AbstractOutgoingWebhook', () => {
const team: Team = {
id: 'team_id',
@@ -74,90 +110,71 @@ describe('components/integrations/AbstractOutgoingWebhook', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<AbstractOutgoingWebhook {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AbstractOutgoingWebhook {...requiredProps}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
test('should not render username in case of enablePostUsernameOverride is false ', () => {
const usernameTrueProps = {...requiredProps};
const wrapper = shallow(<AbstractOutgoingWebhook {...usernameTrueProps}/>);
expect(wrapper.find('#username')).toHaveLength(0);
const {container} = renderWithContext(<AbstractOutgoingWebhook {...usernameTrueProps}/>, initialState as GlobalState);
expect(container.querySelector('#username')).not.toBeInTheDocument();
});
test('should not render post icon override in case of enablePostIconOverride is false ', () => {
const iconUrlTrueProps = {...requiredProps};
const wrapper = shallow(<AbstractOutgoingWebhook {...iconUrlTrueProps}/>);
expect(wrapper.find('#iconURL')).toHaveLength(0);
const {container} = renderWithContext(<AbstractOutgoingWebhook {...iconUrlTrueProps}/>, initialState as GlobalState);
expect(container.querySelector('#iconURL')).not.toBeInTheDocument();
});
test('should render username in case of enablePostUsernameOverride is true ', () => {
const usernameTrueProps = {...requiredProps, enablePostUsernameOverride: true};
const wrapper = shallow(<AbstractOutgoingWebhook {...usernameTrueProps}/>);
expect(wrapper.find('#username')).toHaveLength(1);
const {container} = renderWithContext(<AbstractOutgoingWebhook {...usernameTrueProps}/>, initialState as GlobalState);
expect(container.querySelector('#username')).toBeInTheDocument();
});
test('should render post icon override in case of enablePostIconOverride is true ', () => {
const iconUrlTrueProps = {...requiredProps, enablePostIconOverride: true};
const wrapper = shallow(<AbstractOutgoingWebhook {...iconUrlTrueProps}/>);
expect(wrapper.find('#iconURL')).toHaveLength(1);
const {container} = renderWithContext(<AbstractOutgoingWebhook {...iconUrlTrueProps}/>, initialState as GlobalState);
expect(container.querySelector('#iconURL')).toBeInTheDocument();
});
test('should update state.channelId when on channel change', () => {
const newChannelId = 'new_channel_id';
const evt = {
preventDefault: jest.fn(),
target: {value: newChannelId},
};
const wrapper = shallow(<AbstractOutgoingWebhook {...requiredProps}/>);
wrapper.find(ChannelSelect).simulate('change', evt);
expect(wrapper.state('channelId')).toBe(newChannelId);
test('should update state.channelId when on channel change', async () => {
const {container} = renderWithContext(<AbstractOutgoingWebhook {...requiredProps}/>, initialState as GlobalState);
const channelSelect = container.querySelector('#channelSelect') as HTMLSelectElement;
await userEvent.selectOptions(channelSelect, 'current_channel_id');
expect(channelSelect).toHaveValue('current_channel_id');
});
test('should update state.description when on description change', () => {
const newDescription = 'new_description';
const evt = {
preventDefault: jest.fn(),
target: {value: newDescription},
};
const wrapper = shallow(<AbstractOutgoingWebhook {...requiredProps}/>);
wrapper.find('#description').simulate('change', evt);
expect(wrapper.state('description')).toBe(newDescription);
test('should update state.description when on description change', async () => {
const {container} = renderWithContext(<AbstractOutgoingWebhook {...requiredProps}/>, initialState as GlobalState);
const descriptionInput = container.querySelector('#description') as HTMLInputElement;
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, 'new_description');
expect(descriptionInput).toHaveValue('new_description');
});
test('should update state.username on post username change', () => {
test('should update state.username on post username change', async () => {
const usernameTrueProps = {...requiredProps, enablePostUsernameOverride: true};
const newUsername = 'new_username';
const evt = {
preventDefault: jest.fn(),
target: {value: newUsername},
};
const wrapper = shallow(<AbstractOutgoingWebhook {...usernameTrueProps}/>);
wrapper.find('#username').simulate('change', evt);
expect(wrapper.state('username')).toBe(newUsername);
const {container} = renderWithContext(<AbstractOutgoingWebhook {...usernameTrueProps}/>, initialState as GlobalState);
const usernameInput = container.querySelector('#username') as HTMLInputElement;
await userEvent.type(usernameInput, 'new_username');
expect(usernameInput).toHaveValue('new_username');
});
test('should update state.triggerWhen on selection change', () => {
const wrapper = shallow(<AbstractOutgoingWebhook {...requiredProps}/>);
expect(wrapper.state('triggerWhen')).toBe(0);
const selector = wrapper.find('#triggerWhen');
selector.simulate('change', {target: {value: 1}});
expect(wrapper.state('triggerWhen')).toBe(1);
test('should update state.triggerWhen on selection change', async () => {
const {container} = renderWithContext(<AbstractOutgoingWebhook {...requiredProps}/>, initialState as GlobalState);
const triggerWhenSelect = container.querySelector('#triggerWhen') as HTMLSelectElement;
expect(triggerWhenSelect).toHaveValue('0');
await userEvent.selectOptions(triggerWhenSelect, '1');
expect(triggerWhenSelect).toHaveValue('1');
});
test('should call action function', () => {
const wrapper = shallow(<AbstractOutgoingWebhook {...requiredProps}/>);
wrapper.find('#displayName').simulate('change', {target: {value: 'name'}});
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
test('should call action function', async () => {
const {container} = renderWithContext(<AbstractOutgoingWebhook {...requiredProps}/>, initialState as GlobalState);
const displayNameInput = container.querySelector('#displayName') as HTMLInputElement;
await userEvent.clear(displayNameInput);
await userEvent.type(displayNameInput, 'name');
await userEvent.click(container.querySelector('#saveWebhook') as HTMLButtonElement);
expect(action).toHaveBeenCalled();
expect(action).toHaveBeenCalledTimes(1);
@@ -1,30 +1,305 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/AddCommand should match snapshot 1`] = `
<AbstractCommand
action={[Function]}
footer="Save"
header="Add"
loading="Saving..."
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content row"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/commands"
>
Slash Commands
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
<span>
Add
</span>
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value=""
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the slash command settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="128"
type="text"
value=""
/>
<div
class="form__help"
>
Describe your slash command.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="trigger"
>
Command Trigger Word
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="trigger"
maxlength="128"
placeholder="Command trigger e.g. \\"hello\\" not including the slash"
type="text"
value=""
/>
<div
class="form__help"
>
Specify a trigger word that is not a built-in command, does not contain spaces, and does not begin with the slash character.
</div>
<div
class="form__help"
>
Examples: client, employee, patient, weather
</div>
<div
class="form__help"
>
Reserved:
<a
href="https://mattermost.com/pl/custom-slash-commands?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_command&uid=&sid=&edition=team&server_version="
location="abstract_command"
rel="noopener noreferrer"
target="_blank"
>
See built-in slash commands
</a>
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="url"
>
Request URL
</label>
<div
class="col-md-5 col-sm-8"
>
<input
autocomplete="off"
class="form-control"
id="url"
maxlength="1024"
placeholder="Must start with http:// or https://"
value=""
/>
<div
class="form__help"
>
Specify the callback URL to receive the HTTP POST or GET event request when the slash command is run.
</div>
<div
class="form__help"
>
You can connect commands to
<a
href="https://mattermost.com/pl/outgoing-oauth-connections"
>
outgoing OAuth connections
</a>
.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="method"
>
Request Method
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="method"
>
<option
value="P"
>
POST
</option>
<option
value="G"
>
GET
</option>
</select>
<div
class="form__help"
>
Specify the type of request, either POST or GET, sent to the endpoint that Mattermost hits to reach your application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Response Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="64"
placeholder="Username"
type="text"
value=""
/>
<div
class="form__help"
>
(Optional) Specify the name to use when posting responses for this slash command. Usernames can be up to 22 characters, and contain lowercase letters, numbers, and the symbols "-", "_", and ".". If left blank, your Mattermost username is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconUrl"
>
Response Icon
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconUrl"
maxlength="1024"
placeholder="https://www.example.com/myicon.png"
type="text"
value=""
/>
<div
class="form__help"
>
(Optional) Enter the URL of a .png or .jpg file to use as the icon when posting responses to this slash command. The file must be at least 128 pixels by 128 pixels. If left blank, your profile picture is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocomplete"
>
Autocomplete
</label>
<div
class="col-md-5 col-sm-8 checkbox"
>
<input
id="autocomplete"
type="checkbox"
/>
<div
class="form__help"
>
(Optional) Show your slash command on the autocomplete list when someone types "/" in the input box.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/commands"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveCommand"
type="submit"
>
<span>
Save
</span>
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import AddCommand from 'components/integrations/add_command/add_command';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
describe('components/integrations/AddCommand', () => {
@@ -13,12 +13,12 @@ describe('components/integrations/AddCommand', () => {
const emptyFunction = jest.fn();
const team = TestHelper.getTeamMock({name: 'test'});
const wrapper = shallow(
const {container} = renderWithContext(
<AddCommand
team={team}
actions={{addCommand: emptyFunction}}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,47 +1,193 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/AddIncomingWebhook should match snapshot 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Save",
"id": "add_incoming_webhook.save",
}
}
header={
Object {
"defaultMessage": "Add",
"id": "integrations.add",
}
}
loading={
Object {
"defaultMessage": "Saving...",
"id": "add_incoming_webhook.saving",
}
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "testteamid",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/incoming_webhooks"
>
Incoming Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value=""
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value=""
/>
<div
class="form__help"
>
Describe your incoming webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="current_channel_id"
>
name
</option>
</select>
<div
class="form__help"
>
This is the default public or private channel that receives the webhook payloads. When setting up the webhook, you must belong to the private channel.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="22"
type="text"
value=""
/>
<div
class="form__help"
>
Specify the username this integration will post as. Usernames can be up to 22 characters, and can contain lowercase letters, numbers and the symbols "-", "_", and ".". If left blank, the name specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconURL"
>
Profile Picture
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconURL"
maxlength="1024"
type="text"
value=""
/>
<div
class="form__help"
>
Enter the URL of a .png or .jpg file for the profile picture of this integration when posting. The file should be at least 128 pixels by 128 pixels. If left blank, the profile picture specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/incoming_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveWebhook"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
@@ -9,12 +8,12 @@ import type {DeepPartial} from '@mattermost/types/utilities';
import AddIncomingWebhook from 'components/integrations/add_incoming_webhook/add_incoming_webhook';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
const initialState = {
const initialState: DeepPartial<GlobalState> = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
@@ -43,7 +42,7 @@ const initialState = {
},
},
},
} as DeepPartial<GlobalState>;
};
describe('components/integrations/AddIncomingWebhook', () => {
const createIncomingHook = jest.fn().mockResolvedValue({data: true});
@@ -58,8 +57,8 @@ describe('components/integrations/AddIncomingWebhook', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<AddIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<AddIncomingWebhook {...props}/>, initialState as GlobalState);
expect(container).toMatchSnapshot();
});
test('should have called createIncomingHook', async () => {
@@ -74,15 +73,15 @@ describe('components/integrations/AddIncomingWebhook', () => {
update_at: 0,
id: '',
});
const wrapper = renderWithContext(<AddIncomingWebhook {...props}/>, initialState as GlobalState);
renderWithContext(<AddIncomingWebhook {...props}/>, initialState as GlobalState);
await userEvent.selectOptions(wrapper.getByRole('combobox'), [hook.channel_id]);
await userEvent.type(wrapper.getByLabelText('Title'), hook.display_name);
await userEvent.type(wrapper.getByLabelText('Description'), hook.description);
await userEvent.type(wrapper.getByLabelText('Username'), hook.username);
await userEvent.type(wrapper.getByLabelText('Profile Picture'), hook.icon_url);
await userEvent.selectOptions(screen.getByRole('combobox'), [hook.channel_id]);
await userEvent.type(screen.getByRole('textbox', {name: 'Title'}), hook.display_name);
await userEvent.type(screen.getByRole('textbox', {name: 'Description'}), hook.description);
await userEvent.type(screen.getByRole('textbox', {name: 'Username'}), hook.username);
await userEvent.type(screen.getByRole('textbox', {name: 'Profile Picture'}), hook.icon_url);
await userEvent.click(wrapper.getByText('Save'));
await userEvent.click(screen.getByText('Save'));
expect(createIncomingHook).toHaveBeenCalledTimes(1);
const calledWith = createIncomingHook.mock.calls[0][0];
@@ -1,45 +1,264 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/AddOAuthApp should match snapshot 1`] = `
<AbstractOAuthApp
action={[Function]}
footer={
Object {
"defaultMessage": "Save",
"id": "installed_oauth_apps.save",
}
}
header={
Object {
"defaultMessage": "Add",
"id": "add_oauth_app.header",
}
}
loading={
Object {
"defaultMessage": "Saving...",
"id": "installed_oauth_apps.saving",
}
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "dbcxd9wpzpbpfp8pad78xj12pr",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/oauth2-apps"
>
OAuth 2.0 Applications
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="is_trusted"
>
Is Trusted
</label>
<div
class="col-md-5 col-sm-8"
>
<label
class="radio-inline"
>
<input
name="is_trusted"
type="radio"
value="true"
/>
Yes
</label>
<label
class="radio-inline"
>
<input
checked=""
name="is_trusted"
type="radio"
value="false"
/>
No
</label>
<div
class="form__help"
>
If true, the OAuth 2.0 application is considered trusted by the Mattermost server and does not require the user to accept authorization. If false, a window opens to ask the user to accept or deny the authorization.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="is_public"
>
Is Public Client
</label>
<div
class="col-md-5 col-sm-8"
>
<label
class="radio-inline"
>
<input
name="is_public"
type="radio"
value="true"
/>
Yes
</label>
<label
class="radio-inline"
>
<input
checked=""
name="is_public"
type="radio"
value="false"
/>
No
</label>
<div
class="form__help"
>
If true, the OAuth 2.0 application is a public client (no client secret). Public clients must use PKCE for authorization. If false, the application is a confidential client with a client secret. This setting cannot be changed after the application is created.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="name"
>
Display Name
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="name"
maxlength="64"
type="text"
value=""
/>
<div
class="form__help"
>
Specify the display name, of up to 64 characters, for your OAuth 2.0 application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="512"
type="text"
value=""
/>
<div
class="form__help"
>
Describe your OAuth 2.0 application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="homepage"
>
Homepage
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="homepage"
maxlength="256"
type="url"
value=""
/>
<div
class="form__help"
>
This is the URL for the homepage of the OAuth 2.0 application. Depending on your server configuration, use HTTP or HTTPS in the URL.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="icon_url"
>
Icon URL
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="icon_url"
maxlength="512"
type="url"
value=""
/>
<div
class="form__help"
>
(Optional) The URL of the image used for your OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1024"
rows="3"
/>
<div
class="form__help"
>
The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/oauth2-apps"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveOauthApp"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -1,13 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import AddOAuthApp from 'components/integrations/add_oauth_app/add_oauth_app';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/permissions_gates/system_permission_gate', () => ({children}: {children: React.ReactNode}) => <>{children}</>);
describe('components/integrations/AddOAuthApp', () => {
const emptyFunction = jest.fn();
const team = TestHelper.getTeamMock({
@@ -16,13 +18,13 @@ describe('components/integrations/AddOAuthApp', () => {
});
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AddOAuthApp
team={team}
actions={{addOAuthApp: emptyFunction}}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,48 +1,277 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/AddOutgoingWebhook should match snapshot 1`] = `
<AbstractOutgoingWebhook
action={[Function]}
enablePostIconOverride={false}
enablePostUsernameOverride={false}
footer={
Object {
"defaultMessage": "Save",
"id": "add_outgoing_webhook.save",
}
}
header={
Object {
"defaultMessage": "Add",
"id": "integrations.add",
}
}
loading={
Object {
"defaultMessage": "Saving...",
"id": "add_outgoing_webhook.saving",
}
}
renderExtra=""
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "testteamid",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/outgoing_webhooks"
>
Outgoing Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value=""
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value=""
/>
<div
class="form__help"
>
Describe your outgoing webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="contentType"
>
Content Type
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
>
<option
value="application/x-www-form-urlencoded"
>
application/x-www-form-urlencoded
</option>
<option
value="application/json"
>
application/json
</option>
</select>
<div
class="form__help"
>
Specify the content type by which to send the request.
</div>
<div
class="form__help"
>
For the server to encode the parameters in a URL format in the request body, select application/x-www-form-urlencoded.
</div>
<div
class="form__help"
>
For the server to format the request body as JSON, select application/json.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="current_channel_id"
>
name
</option>
</select>
<div
class="form__help"
>
This field is optional if you specify at least one trigger word. Specify the public channel that delivers the payload to the webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger Words (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="triggerWords"
maxlength="1000"
rows="3"
/>
<div
class="form__help"
>
Specify the trigger words that send an HTTP POST request to your application. The trigger can be for the channel, the outgoing webhook, or both. If you select only Channel, trigger words are optional. If you select both, the message must match both values.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger When
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="triggerWhen"
>
<option
value="0"
>
First word matches a trigger word exactly
</option>
<option
value="1"
>
First word starts with a trigger word
</option>
</select>
<div
class="form__help"
>
Specify when to trigger the outgoing webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1000"
rows="3"
/>
<div
class="form__help"
>
Specify the URL that the messages will be sent to. If the URL is private, add it as a
<a
href="https://mattermost.com/pl/default-allow-untrusted-internal-connections?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_outgoing_webhook&uid=&sid=&edition=team&server_version="
location="abstract_outgoing_webhook"
rel="noopener noreferrer"
target="_blank"
>
trusted internal connection
</a>
.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/outgoing_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveWebhook"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -1,13 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {DeepPartial} from '@mattermost/types/utilities';
import AddOutgoingWebhook from 'components/integrations/add_outgoing_webhook/add_outgoing_webhook';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
const initialState: DeepPartial<GlobalState> = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: TestHelper.getChannelMock({
id: 'current_channel_id',
team_id: 'testteamid',
type: 'O' as ChannelType,
name: 'current_channel',
}),
},
myMembers: {
current_channel_id: TestHelper.getChannelMembershipMock({channel_id: 'current_channel_id'}),
},
channelsInTeam: {
testteamid: new Set(['current_channel_id']),
},
},
teams: {
currentTeamId: 'testteamid',
teams: {
testteamid: TestHelper.getTeamMock({id: 'testteamid'}),
},
myMembers: {
testteamid: TestHelper.getTeamMembershipMock({roles: 'team_roles'}),
},
},
},
};
describe('components/integrations/AddOutgoingWebhook', () => {
test('should match snapshot', () => {
const emptyFunction = jest.fn();
@@ -16,14 +52,15 @@ describe('components/integrations/AddOutgoingWebhook', () => {
name: 'test',
});
const wrapper = shallow(
const {container} = renderWithContext(
<AddOutgoingWebhook
team={team}
actions={{createOutgoingHook: emptyFunction}}
enablePostUsernameOverride={false}
enablePostIconOverride={false}
/>,
initialState as GlobalState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
});
@@ -1,362 +1,281 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/bots/AddBot blank 1`] = `
<div
className="backstage-content"
>
<BackstageHeader>
<Link
to="/DN/integrations/bots"
>
<MemoizedFormattedMessage
defaultMessage="Bot Accounts"
id="bots.manage.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Add"
id="bots.manage.add.add"
/>
</BackstageHeader>
<div>
<div
className="backstage-form"
class="backstage-content"
>
<form
className="form-horizontal"
onSubmit={[Function]}
<div
class="backstage-header"
>
<div
className="form-group"
<h1>
<a
href="/DN/integrations/bots"
>
Bot Accounts
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<label
className="control-label col-sm-4"
htmlFor="username"
>
<MemoizedFormattedMessage
defaultMessage="Username"
id="bots.add.username"
/>
</label>
<div
className="col-md-5 col-sm-8"
class="form-group"
>
<input
className="form-control"
id="username"
maxLength={22}
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
<label
class="control-label col-sm-4"
for="username"
>
<MemoizedFormattedMessage
defaultMessage="You can use lowercase letters, numbers, periods, dashes, and underscores."
id="bot.add.username.help"
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="boticon"
>
<MemoizedFormattedMessage
defaultMessage="Bot Icon"
id="bots.add.icon"
/>
</label>
<div
className="col-md-5 col-sm-8"
>
Username
</label>
<div
className="bot-img-container"
class="col-md-5 col-sm-8"
>
<img
alt="bot image"
className="bot-img"
src=""
style={
Object {
"transform": "",
"transformOrigin": "",
}
}
/>
</div>
<div
className="btn btn-primary btn-file"
>
<MemoizedFormattedMessage
defaultMessage="Upload Image"
id="bots.image.upload"
/>
<input
accept=".jpeg,.jpg,.png,.bmp"
className="btn-file__input"
onChange={[Function]}
type="file"
class="form-control"
id="username"
maxlength="22"
type="text"
value=""
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="displayName"
>
<MemoizedFormattedMessage
defaultMessage="Display Name"
id="bots.add.displayName"
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="displayName"
maxLength={64}
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="(Optional) You can choose to display your bot's full name rather than its username."
id="bot.add.display_name.help"
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="description"
>
<MemoizedFormattedMessage
defaultMessage="Description"
id="bot.add.description"
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="description"
maxLength={1024}
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="(Optional) Let others know what this bot does."
id="bot.add.description.help"
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="role"
>
<MemoizedFormattedMessage
defaultMessage="Role"
id="bot.add.role"
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<select
className="form-control"
disabled={false}
onChange={[Function]}
value="Member"
>
<option
value="Member"
<div
class="form__help"
>
Member
</option>
<option
value="System Admin"
>
System Admin
</option>
</select>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="Choose what role the bot should have."
id="bot.add.role.help"
/>
You can use lowercase letters, numbers, periods, dashes, and underscores.
</div>
</div>
</div>
</div>
<div
className="row bot-profile__section"
>
<div
className="col-md-5 col-sm-8 col-sm-offset-4"
class="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Select additional permissions for the account. <link>Read more about roles and permissions</link>."
id="admin.manage_roles.botAdditionalRoles"
values={
Object {
"link": [Function],
}
}
/>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="postAll"
>
<MemoizedFormattedMessage
defaultMessage="post:all"
id="bot.add.post_all"
/>
</label>
<div
className="col-md-5 col-sm-8 checkbox"
>
<div
className="checkbox no-padding"
<label
class="control-label col-sm-4"
for="boticon"
>
<label
htmlFor="postAll"
Bot Icon
</label>
<div
class="col-md-5 col-sm-8"
>
<div
class="bot-img-container"
>
<img
alt="bot image"
class="bot-img"
src=""
/>
</div>
<div
class="btn btn-primary btn-file"
>
Upload Image
<input
checked={false}
disabled={false}
id="postAll"
onChange={[Function]}
type="checkbox"
accept=".jpeg,.jpg,.png,.bmp"
class="btn-file__input"
type="file"
/>
<MemoizedFormattedMessage
defaultMessage="Enabled"
id="bot.add.post_all.enabled"
/>
</label>
</div>
<div
className="form__help"
>
<MemoizedFormattedMessage
defaultMessage="Bot will have access to post to all Mattermost channels including direct messages."
id="bot.add.post_all.help"
/>
</div>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="postChannels"
>
<MemoizedFormattedMessage
defaultMessage="post:channels"
id="bot.add.post_channels"
/>
</label>
<div
className="col-md-5 col-sm-8 checkbox"
class="form-group"
>
<div
className="checkbox no-padding"
<label
class="control-label col-sm-4"
for="displayName"
>
<label
htmlFor="postChannels"
>
<input
checked={false}
disabled={false}
id="postChannels"
onChange={[Function]}
type="checkbox"
/>
<MemoizedFormattedMessage
defaultMessage="Enabled"
id="bot.add.post_channels.enabled"
/>
</label>
</div>
Display Name
</label>
<div
className="form__help"
class="col-md-5 col-sm-8"
>
<MemoizedFormattedMessage
defaultMessage="Bot will have access to post to all Mattermost public channels."
id="bot.add.post_channels.help"
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value=""
/>
<div
class="form__help"
>
(Optional) You can choose to display your bot's full name rather than its username.
</div>
</div>
</div>
</div>
<div
className="backstage-form__footer"
>
<Memo(FormError)
errors={
Array [
"",
]
}
type="backstage"
/>
<Link
className="btn btn-tertiary"
to="/DN/integrations/bots"
<div
class="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Cancel"
id="bots.manage.add.cancel"
/>
</Link>
<Memo(SpinnerButton)
className="btn btn-primary"
id="saveBot"
onClick={[Function]}
spinning={false}
spinningText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Creating..."
id="bots.manage.add.creating"
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="1024"
type="text"
value=""
/>
}
type="submit"
<div
class="form__help"
>
(Optional) Let others know what this bot does.
</div>
</div>
</div>
<div
class="form-group"
>
<MemoizedFormattedMessage
defaultMessage="Create Bot Account"
id="bots.manage.add.create"
/>
</Memo(SpinnerButton)>
</div>
</form>
<label
class="control-label col-sm-4"
for="role"
>
Role
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
>
<option
value="Member"
>
Member
</option>
<option
value="System Admin"
>
System Admin
</option>
</select>
<div
class="form__help"
>
Choose what role the bot should have.
</div>
</div>
</div>
<div
class="row bot-profile__section"
>
<div
class="col-md-5 col-sm-8 col-sm-offset-4"
>
Select additional permissions for the account.
<a
href="https://mattermost.com/pl/personal-access-tokens?utm_source=mattermost&utm_medium=in-product&utm_content=add_bot&uid=&sid=&edition=team&server_version="
location="add_bot"
rel="noopener noreferrer"
target="_blank"
>
Read more about roles and permissions
</a>
.
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="postAll"
>
post:all
</label>
<div
class="col-md-5 col-sm-8 checkbox"
>
<div
class="checkbox no-padding"
>
<label
for="postAll"
>
<input
id="postAll"
type="checkbox"
/>
Enabled
</label>
</div>
<div
class="form__help"
>
Bot will have access to post to all Mattermost channels including direct messages.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="postChannels"
>
post:channels
</label>
<div
class="col-md-5 col-sm-8 checkbox"
>
<div
class="checkbox no-padding"
>
<label
for="postChannels"
>
<input
id="postChannels"
type="checkbox"
/>
Enabled
</label>
</div>
<div
class="form__help"
>
Bot will have access to post to all Mattermost public channels.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/DN/integrations/bots"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveBot"
type="submit"
>
Create Bot Account
</button>
</div>
</form>
</div>
</div>
</div>
`;
@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import AddBot from './add_bot';
@@ -21,7 +21,7 @@ describe('components/integrations/bots/AddBot', () => {
};
it('blank', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<AddBot
maxFileSize={100}
team={team}
@@ -29,30 +29,23 @@ describe('components/integrations/bots/AddBot', () => {
actions={actions}
/>,
);
expect(wrapper.containsMatchingElement(
<input
id='username'
value={''}
/>,
)).toEqual(true);
expect(wrapper.containsMatchingElement(
<input
id='displayName'
value={''}
/>,
)).toEqual(true);
expect(wrapper.containsMatchingElement(
<input
id='description'
value={''}
/>,
)).toEqual(true);
expect(wrapper).toMatchSnapshot();
const usernameInput = container.querySelector('#username') as HTMLInputElement;
const displayNameInput = container.querySelector('#displayName') as HTMLInputElement;
const descriptionInput = container.querySelector('#description') as HTMLInputElement;
expect(usernameInput).toBeInTheDocument();
expect(usernameInput.value).toBe('');
expect(displayNameInput).toBeInTheDocument();
expect(displayNameInput.value).toBe('');
expect(descriptionInput).toBeInTheDocument();
expect(descriptionInput.value).toBe('');
expect(container).toMatchSnapshot();
});
it('edit bot', () => {
const bot = TestHelper.getBotMock({});
const wrapper = shallow(
const {container} = renderWithContext(
<AddBot
bot={bot}
maxFileSize={100}
@@ -61,23 +54,16 @@ describe('components/integrations/bots/AddBot', () => {
actions={actions}
/>,
);
expect(wrapper.containsMatchingElement(
<input
id='username'
value={bot.username}
/>,
)).toEqual(true);
expect(wrapper.containsMatchingElement(
<input
id='displayName'
value={bot.display_name}
/>,
)).toEqual(true);
expect(wrapper.containsMatchingElement(
<input
id='description'
value={bot.description}
/>,
)).toEqual(true);
const usernameInput = container.querySelector('#username') as HTMLInputElement;
const displayNameInput = container.querySelector('#displayName') as HTMLInputElement;
const descriptionInput = container.querySelector('#description') as HTMLInputElement;
expect(usernameInput).toBeInTheDocument();
expect(usernameInput.value).toBe(bot.username);
expect(displayNameInput).toBeInTheDocument();
expect(displayNameInput.value).toBe(bot.display_name);
expect(descriptionInput).toBeInTheDocument();
expect(descriptionInput.value).toBe(bot.description);
});
});
@@ -1,14 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {generateId} from 'mattermost-redux/utils/helpers';
import Markdown from 'components/markdown';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper as UtilsTestHelper} from 'utils/test_helper';
import Bot from './bot';
@@ -27,7 +24,7 @@ describe('components/integrations/bots/Bot', () => {
it('regular bot', () => {
const bot = UtilsTestHelper.getBotMock({user_id: '1'});
const user = UtilsTestHelper.getUserMock({id: bot.user_id});
const wrapper = shallow(
renderWithContext(
<Bot
bot={bot}
user={user}
@@ -39,41 +36,20 @@ describe('components/integrations/bots/Bot', () => {
/>,
);
expect(wrapper.contains(bot.display_name + ' (@' + bot.username + ')')).toEqual(true);
expect(wrapper.contains(<Markdown message={bot.description}/>)).toEqual(true);
expect(wrapper.contains('plugin')).toEqual(true);
expect(screen.getByText(/\(@\)/)).toBeInTheDocument();
expect(screen.getByText(/plugin/)).toBeInTheDocument();
// if bot managed by plugin, remove ability to edit from UI
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.create_token'
defaultMessage='Create New Token'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='bots.manage.edit'
defaultMessage='Edit'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.disable'
defaultMessage='Disable'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.enable'
defaultMessage='Enable'
/>,
)).toEqual(false);
expect(screen.queryByText('Create New Token')).not.toBeInTheDocument();
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
expect(screen.queryByText(/^Disable$/)).not.toBeInTheDocument();
expect(screen.queryByText(/^Enable$/)).not.toBeInTheDocument();
});
it('app bot', () => {
const bot = UtilsTestHelper.getBotMock({user_id: '1'});
const user = UtilsTestHelper.getUserMock({id: bot.user_id});
const wrapper = shallow(
renderWithContext(
<Bot
bot={bot}
user={user}
@@ -85,42 +61,21 @@ describe('components/integrations/bots/Bot', () => {
/>,
);
expect(wrapper.contains(bot.display_name + ' (@' + bot.username + ')')).toEqual(true);
expect(wrapper.contains(<Markdown message={bot.description}/>)).toEqual(true);
expect(wrapper.contains('Apps Framework')).toEqual(true);
expect(screen.getByText(/\(@\)/)).toBeInTheDocument();
expect(screen.getByText(/Apps Framework/)).toBeInTheDocument();
// if bot managed by plugin, remove ability to edit from UI
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.create_token'
defaultMessage='Create New Token'
/>,
)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='bots.manage.edit'
defaultMessage='Edit'
/>,
)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.disable'
defaultMessage='Disable'
/>,
)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.enable'
defaultMessage='Enable'
/>,
)).toEqual(false);
// if bot managed by app framework, ability to edit from UI is retained
expect(screen.getByText('Create New Token')).toBeInTheDocument();
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Disable')).toBeInTheDocument();
expect(screen.queryByText(/^Enable$/)).not.toBeInTheDocument();
});
it('disabled bot', () => {
const bot = UtilsTestHelper.getBotMock({user_id: '1'});
bot.delete_at = 100; // disabled
const user = UtilsTestHelper.getUserMock({id: bot.user_id});
const wrapper = shallow(
renderWithContext(
<Bot
bot={bot}
user={user}
@@ -131,40 +86,19 @@ describe('components/integrations/bots/Bot', () => {
fromApp={false}
/>,
);
expect(wrapper.contains(bot.display_name + ' (@' + bot.username + ')')).toEqual(true);
expect(wrapper.contains(<Markdown message={bot.description}/>)).toEqual(true);
expect(wrapper.contains('plugin')).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.create_token'
defaultMessage='Create New Token'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='bots.manage.edit'
defaultMessage='Edit'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.disable'
defaultMessage='Disable'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.enable'
defaultMessage='Enable'
/>,
)).toEqual(true);
expect(screen.getByText(/\(@\)/)).toBeInTheDocument();
expect(screen.getByText(/plugin/)).toBeInTheDocument();
expect(screen.queryByText('Create New Token')).not.toBeInTheDocument();
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
expect(screen.queryByText(/^Disable$/)).not.toBeInTheDocument();
expect(screen.getByText('Enable')).toBeInTheDocument();
});
it('bot with owner', () => {
const bot = UtilsTestHelper.getBotMock({user_id: '1', owner_id: '1'});
const owner = UtilsTestHelper.getUserMock({id: bot.owner_id});
const user = UtilsTestHelper.getUserMock({id: bot.user_id});
const wrapper = shallow(
renderWithContext(
<Bot
bot={bot}
owner={owner}
@@ -175,28 +109,13 @@ describe('components/integrations/bots/Bot', () => {
fromApp={false}
/>,
);
expect(wrapper.contains(owner.username)).toEqual(true);
expect(wrapper.contains('plugin')).toEqual(false);
expect(screen.getByText(new RegExp(owner.username))).toBeInTheDocument();
expect(screen.queryByText(/plugin/)).not.toBeInTheDocument();
// if bot is not managed by plugin, ability to edit from UI is retained
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.create_token'
defaultMessage='Create New Token'
/>,
)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='bots.manage.edit'
defaultMessage='Edit'
/>,
)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='bot.manage.disable'
defaultMessage='Disable'
/>,
)).toEqual(true);
expect(screen.getByText('Create New Token')).toBeInTheDocument();
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Disable')).toBeInTheDocument();
});
it('bot with access tokens', () => {
@@ -210,7 +129,7 @@ describe('components/integrations/bots/Bot', () => {
}),
};
const wrapper = shallow(
renderWithContext(
<Bot
bot={bot}
owner={undefined}
@@ -222,19 +141,9 @@ describe('components/integrations/bots/Bot', () => {
/>,
);
expect(wrapper.contains(tokenId)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='user.settings.tokens.deactivate'
defaultMessage='Disable'
/>,
)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='user.settings.tokens.activate'
defaultMessage='Enable'
/>,
)).toEqual(false);
expect(screen.getByText(tokenId)).toBeInTheDocument();
expect(screen.getByText(/^Disable$/)).toBeInTheDocument();
expect(screen.queryByText(/^Enable$/)).not.toBeInTheDocument();
});
it('bot with disabled access tokens', () => {
@@ -250,7 +159,7 @@ describe('components/integrations/bots/Bot', () => {
}),
};
const wrapper = shallow(
renderWithContext(
<Bot
bot={bot}
owner={undefined}
@@ -262,18 +171,8 @@ describe('components/integrations/bots/Bot', () => {
/>,
);
expect(wrapper.contains(tokenId)).toEqual(true);
expect(wrapper.contains(
<FormattedMessage
id='user.settings.tokens.deactivate'
defaultMessage='Disable'
/>,
)).toEqual(false);
expect(wrapper.contains(
<FormattedMessage
id='user.settings.tokens.activate'
defaultMessage='Enable'
/>,
)).toEqual(true);
expect(screen.getByText(tokenId)).toBeInTheDocument();
expect(screen.queryByText(/^Disable$/)).not.toBeInTheDocument();
expect(screen.getByText(/^Enable$/)).toBeInTheDocument();
});
});
@@ -1,18 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {Bot} from '@mattermost/types/bots';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import Bot from './bot';
import Bots from './bots';
describe('components/integrations/bots/Bots', () => {
const team = TestHelper.getTeamMock();
const actions = {
loadBots: jest.fn().mockReturnValue(Promise.resolve({})),
loadBots: jest.fn().mockReturnValue(Promise.resolve({data: []})),
getUserAccessTokensForUser: jest.fn(),
createUserAccessToken: jest.fn(),
revokeUserAccessToken: jest.fn(),
@@ -24,150 +25,98 @@ describe('components/integrations/bots/Bots', () => {
fetchAppsBotIDs: jest.fn(),
};
it('bots', () => {
const bot1 = TestHelper.getBotMock({user_id: '1'});
const bot2 = TestHelper.getBotMock({user_id: '2'});
const bot3 = TestHelper.getBotMock({user_id: '3'});
const bots = {
[bot1.user_id]: bot1,
[bot2.user_id]: bot2,
[bot3.user_id]: bot3,
};
const users = {
[bot1.user_id]: TestHelper.getUserMock({id: bot1.user_id}),
[bot2.user_id]: TestHelper.getUserMock({id: bot2.user_id}),
[bot3.user_id]: TestHelper.getUserMock({id: bot3.user_id}),
};
function createBotsAndUsers(count: number) {
const bots: Record<string, Bot> = {};
const users: Record<string, ReturnType<typeof TestHelper.getUserMock>> = {};
const botList: Bot[] = [];
for (let i = 1; i <= count; i++) {
const bot = TestHelper.getBotMock({user_id: String(i), username: `bot${i}`, display_name: `Bot ${i}`, delete_at: 0});
bots[bot.user_id] = bot;
users[bot.user_id] = TestHelper.getUserMock({id: bot.user_id});
botList.push(bot);
}
const loadBots = jest.fn().mockReturnValue(Promise.resolve({data: botList}));
return {bots, users, loadBots};
}
const wrapperFull = shallow(
// BackstageList passes filterLowered as a DOM prop which triggers a React warning
let consoleErrorSpy: jest.SpyInstance;
beforeEach(() => {
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
it('bots', async () => {
const {bots, users, loadBots} = createBotsAndUsers(3);
const {container} = renderWithContext(
<Bots
bots={bots}
team={team}
accessTokens={{}}
owners={{}}
users={users}
actions={actions}
actions={{...actions, loadBots}}
appsEnabled={false}
appsBotIDs={[]}
/>,
);
wrapperFull.instance().setState({loading: false});
const wrapper = shallow(<div>{(wrapperFull.instance() as Bots).bots()[0]}</div>);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot1.user_id}
bot={bot1}
owner={undefined}
user={users[bot1.user_id]}
accessTokens={{}}
team={team}
actions={actions}
fromApp={false}
/>,
)).toEqual(true);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot2.user_id}
bot={bot2}
owner={undefined}
user={users[bot2.user_id]}
accessTokens={{}}
team={team}
actions={actions}
fromApp={false}
/>,
)).toEqual(true);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot3.user_id}
bot={bot3}
owner={undefined}
user={users[bot3.user_id]}
accessTokens={{}}
team={team}
actions={actions}
fromApp={false}
/>,
)).toEqual(true);
await waitFor(() => {
expect(screen.getByText(/Bot 1 \(@bot1\)/)).toBeInTheDocument();
expect(screen.getByText(/Bot 2 \(@bot2\)/)).toBeInTheDocument();
expect(screen.getByText(/Bot 3 \(@bot3\)/)).toBeInTheDocument();
});
// All should show plugin as managed-by since no owner
const managedByDivs = container.querySelectorAll('.light.small');
expect(managedByDivs.length).toBe(3);
managedByDivs.forEach((div) => {
expect(div.textContent).toContain('plugin');
});
});
it('bots with bots from apps', () => {
const bot1 = TestHelper.getBotMock({user_id: '1'});
const bot2 = TestHelper.getBotMock({user_id: '2'});
const bot3 = TestHelper.getBotMock({user_id: '3'});
const bots = {
[bot1.user_id]: bot1,
[bot2.user_id]: bot2,
[bot3.user_id]: bot3,
};
const users = {
[bot1.user_id]: TestHelper.getUserMock({id: bot1.user_id}),
[bot2.user_id]: TestHelper.getUserMock({id: bot2.user_id}),
[bot3.user_id]: TestHelper.getUserMock({id: bot3.user_id}),
};
it('bots with bots from apps', async () => {
const {bots, users, loadBots} = createBotsAndUsers(3);
const wrapperFull = shallow(
const {container} = renderWithContext(
<Bots
bots={bots}
team={team}
accessTokens={{}}
owners={{}}
users={users}
actions={actions}
actions={{...actions, loadBots}}
appsEnabled={true}
appsBotIDs={[bot3.user_id]}
appsBotIDs={['3']}
/>,
);
wrapperFull.instance().setState({loading: false});
const wrapper = shallow(<div>{(wrapperFull.instance() as Bots).bots()[0]}</div>);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot1.user_id}
bot={bot1}
owner={undefined}
user={users[bot1.user_id]}
accessTokens={{}}
team={team}
actions={actions}
fromApp={false}
/>,
)).toEqual(true);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot2.user_id}
bot={bot2}
owner={undefined}
user={users[bot2.user_id]}
accessTokens={{}}
team={team}
actions={actions}
fromApp={false}
/>,
)).toEqual(true);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot3.user_id}
bot={bot3}
owner={undefined}
user={users[bot3.user_id]}
accessTokens={{}}
team={team}
actions={actions}
fromApp={true}
/>,
)).toEqual(true);
await waitFor(() => {
expect(screen.getByText(/Bot 1 \(@bot1\)/)).toBeInTheDocument();
expect(screen.getByText(/Bot 2 \(@bot2\)/)).toBeInTheDocument();
expect(screen.getByText(/Bot 3 \(@bot3\)/)).toBeInTheDocument();
});
// Check managed-by for each bot via DOM
const managedByDivs = container.querySelectorAll('.light.small');
expect(managedByDivs.length).toBe(3);
const managedByTexts = Array.from(managedByDivs).map((div) => div.textContent);
expect(managedByTexts.filter((t) => t?.includes('Apps Framework')).length).toBe(1);
expect(managedByTexts.filter((t) => t?.includes('plugin')).length).toBe(2);
});
it('bot owner tokens', () => {
const bot1 = TestHelper.getBotMock({user_id: '1', owner_id: '1'});
it('bot owner tokens', async () => {
const bot1 = TestHelper.getBotMock({user_id: '1', owner_id: '1', username: 'bot1', display_name: 'Bot 1', delete_at: 0});
const bots = {
[bot1.user_id]: bot1,
};
const owner = TestHelper.getUserMock({id: bot1.owner_id});
const owner = TestHelper.getUserMock({id: bot1.owner_id, username: 'owner1'});
const user = TestHelper.getUserMock({id: bot1.user_id});
const passedTokens = {
@@ -186,32 +135,28 @@ describe('components/integrations/bots/Bots', () => {
[bot1.user_id]: passedTokens,
};
const wrapperFull = shallow(
const loadBots = jest.fn().mockReturnValue(Promise.resolve({data: Object.values(bots)}));
const {container} = renderWithContext(
<Bots
bots={bots}
team={team}
accessTokens={tokens}
owners={owners}
users={users}
actions={actions}
actions={{...actions, loadBots}}
appsEnabled={false}
appsBotIDs={[]}
/>,
);
wrapperFull.instance().setState({loading: false});
const wrapper = shallow(<div>{(wrapperFull.instance() as Bots).bots()[0]}</div>);
expect(wrapper.find('EnabledSection').shallow().contains(
<Bot
key={bot1.user_id}
bot={bot1}
owner={owner}
user={user}
accessTokens={passedTokens}
team={team}
actions={actions}
fromApp={false}
/>,
)).toEqual(true);
await waitFor(() => {
expect(screen.getByText(/Bot 1 \(@bot1\)/)).toBeInTheDocument();
});
// Owner username should be shown in managed-by section
const managedByDiv = container.querySelector('.light.small');
expect(managedByDiv).toBeInTheDocument();
expect(managedByDiv!.textContent).toContain('owner1');
});
});
@@ -1,510 +1,441 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/ConfirmIntegration should match snapshot, bad integration type case 1`] = `""`;
exports[`components/integrations/ConfirmIntegration should match snapshot, bad integration type case 1`] = `<div />`;
exports[`components/integrations/ConfirmIntegration should match snapshot, commands case 1`] = `
<div
className="backstage-content row"
>
<BackstageHeader>
<Link
to="/team_test/integrations/commands"
>
<MemoizedFormattedMessage
defaultMessage="Slash Commands"
id="slash_commands.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Add"
id="integrations.add"
/>
</BackstageHeader>
<div>
<div
className="backstage-form backstage-form__confirmation"
class="backstage-content row"
>
<h4
className="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Setup Successful"
id="integrations.successful"
/>
</h4>
<p>
<MemoizedFormattedMessage
defaultMessage="Your slash command is set up. The following token will be sent in the outgoing payload. Please use it to verify the request came from your Mattermost team (details at <link>Slash Commands</link>)."
id="add_command.doneHelp"
values={
Object {
"link": [Function],
}
}
/>
</p>
<p
className="word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="<b>Token</b>: {token}"
id="add_command.token"
values={
Object {
"b": [Function],
"token": <code>
jb6oyqh95irpbx8fo9zmndkp1r
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="jb6oyqh95irpbx8fo9zmndkp1r"
/>
</p>
<div
className="backstage-form__footer"
class="backstage-header"
>
<Link
className="btn btn-primary"
id="doneButton"
to="/team_test/integrations/commands"
type="submit"
<h1>
<a
href="/team_test/integrations/commands"
>
Slash Commands
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form backstage-form__confirmation"
>
<h4
class="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Done"
id="integrations.done"
Setup Successful
</h4>
<p>
Your slash command is set up. The following token will be sent in the outgoing payload. Please use it to verify the request came from your Mattermost team (details at
<a
href="https://mattermost.com/pl/setup-custom-slash-commands?utm_source=mattermost&utm_medium=in-product&utm_content=confirm_integration&uid=currentUserId&sid=&edition=team&server_version="
location="confirm_integration"
rel="noopener noreferrer"
target="_blank"
>
Slash Commands
</a>
).
</p>
<p
class="word-break--all"
>
<b>
Token
</b>
:
<code>
jb6oyqh95irpbx8fo9zmndkp1r
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</Link>
</p>
<div
class="backstage-form__footer"
>
<a
class="btn btn-primary"
href="/team_test/integrations/commands"
id="doneButton"
type="submit"
>
Done
</a>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/ConfirmIntegration should match snapshot, incomingHooks case 1`] = `
<div
className="backstage-content row"
>
<BackstageHeader>
<Link
to="/team_test/integrations/incoming_webhooks"
>
<MemoizedFormattedMessage
defaultMessage="Incoming Webhooks"
id="incoming_webhooks.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Add"
id="integrations.add"
/>
</BackstageHeader>
<div>
<div
className="backstage-form backstage-form__confirmation"
class="backstage-content row"
>
<h4
className="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Setup Successful"
id="integrations.successful"
/>
</h4>
<p>
<MemoizedFormattedMessage
defaultMessage="Your incoming webhook is set up. Please send data to the following URL (details at <link>Incoming Webhooks</link>)."
id="add_incoming_webhook.doneHelp"
values={
Object {
"link": [Function],
}
}
/>
</p>
<p
className="word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="<b>URL</b>: {url}"
id="add_incoming_webhook.url"
values={
Object {
"b": [Function],
"url": <code>
http://localhost:8065/hooks/r5tpgt4iepf45jt768jz84djic
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="http://localhost:8065/hooks/r5tpgt4iepf45jt768jz84djic"
/>
</p>
<div
className="backstage-form__footer"
class="backstage-header"
>
<Link
className="btn btn-primary"
id="doneButton"
to="/team_test/integrations/incoming_webhooks"
type="submit"
<h1>
<a
href="/team_test/integrations/incoming_webhooks"
>
Incoming Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form backstage-form__confirmation"
>
<h4
class="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Done"
id="integrations.done"
Setup Successful
</h4>
<p>
Your incoming webhook is set up. Please send data to the following URL (details at
<a
href="https://mattermost.com/pl/setup-incoming-webhooks?utm_source=mattermost&utm_medium=in-product&utm_content=confirm_integration&uid=currentUserId&sid=&edition=team&server_version="
location="confirm_integration"
rel="noopener noreferrer"
target="_blank"
>
Incoming Webhooks
</a>
).
</p>
<p
class="word-break--all"
>
<b>
URL
</b>
:
<code>
http://localhost:8065/hooks/r5tpgt4iepf45jt768jz84djic
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</Link>
</p>
<div
class="backstage-form__footer"
>
<a
class="btn btn-primary"
href="/team_test/integrations/incoming_webhooks"
id="doneButton"
type="submit"
>
Done
</a>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/ConfirmIntegration should match snapshot, oauthApps case 1`] = `
<div
className="backstage-content row"
>
<BackstageHeader>
<Link
to="/team_test/integrations/oauth2-apps"
>
<MemoizedFormattedMessage
defaultMessage="OAuth 2.0 Applications"
id="installed_oauth2_apps.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Add"
id="integrations.add"
/>
</BackstageHeader>
<div>
<div
className="backstage-form backstage-form__confirmation"
class="backstage-content row"
>
<h4
className="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Setup Successful"
id="integrations.successful"
/>
</h4>
<p
key="add_oauth_app.doneHelp"
>
<MemoizedFormattedMessage
defaultMessage="Your OAuth 2.0 application is set up. Please use the following Client ID and Client Secret when requesting authorization for your application (details at <link>oAuth 2.0 Applications</link>)."
id="add_oauth_app.doneHelp"
values={
Object {
"link": [Function],
}
}
/>
</p>
<p
key="add_oauth_app.clientId"
>
<MemoizedFormattedMessage
defaultMessage="<b>Client ID</b>: {id}"
id="add_oauth_app.clientId"
values={
Object {
"b": [Function],
"id": <code>
r5tpgt4iepf45jt768jz84djic
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Client Id",
"id": "integrations.copy_client_id",
}
}
value="r5tpgt4iepf45jt768jz84djic"
/>
<br />
<MemoizedFormattedMessage
defaultMessage="<b>Client Secret</b>: {secret}"
id="add_oauth_app.clientSecret"
values={
Object {
"b": [Function],
"secret": <code>
&lt;==secret==&gt;
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Client Secret",
"id": "integrations.copy_client_secret",
}
}
value="<==secret==>"
/>
</p>
<p
key="add_oauth_app.doneUrlHelp"
>
<MemoizedFormattedMessage
defaultMessage="Here are your authorized redirect URLs."
id="add_oauth_app.doneUrlHelp"
/>
</p>
<p
className="word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="<b>URL(s)</b>: {url}"
id="add_oauth_app.url"
values={
Object {
"b": [Function],
"url": "https://someCallback, https://anotherCallback",
}
}
/>
</p>
<div
className="backstage-form__footer"
class="backstage-header"
>
<Link
className="btn btn-primary"
id="doneButton"
to="/team_test/integrations/oauth2-apps"
type="submit"
<h1>
<a
href="/team_test/integrations/oauth2-apps"
>
OAuth 2.0 Applications
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form backstage-form__confirmation"
>
<h4
class="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Done"
id="integrations.done"
Setup Successful
</h4>
<p>
Your OAuth 2.0 application is set up. Please use the following Client ID and Client Secret when requesting authorization for your application (details at
<a
href="https://mattermost.com/pl/setup-oauth-2.0?utm_source=mattermost&utm_medium=in-product&utm_content=confirm_integration&uid=currentUserId&sid=&edition=team&server_version="
location="confirm_integration"
rel="noopener noreferrer"
target="_blank"
>
oAuth 2.0 Applications
</a>
).
</p>
<p>
<b>
Client ID
</b>
:
<code>
r5tpgt4iepf45jt768jz84djic
</code>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</Link>
<br />
<b>
Client Secret
</b>
:
<code>
&lt;==secret==&gt;
</code>
<button
aria-label="Copy Client Secret"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</p>
<p>
Here are your authorized redirect URLs.
</p>
<p
class="word-break--all"
>
<b>
URL(s)
</b>
: https://someCallback, https://anotherCallback
</p>
<div
class="backstage-form__footer"
>
<a
class="btn btn-primary"
href="/team_test/integrations/oauth2-apps"
id="doneButton"
type="submit"
>
Done
</a>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/ConfirmIntegration should match snapshot, outgoingHooks and bad identifier case 1`] = `""`;
exports[`components/integrations/ConfirmIntegration should match snapshot, outgoingHooks and bad identifier case 1`] = `<div />`;
exports[`components/integrations/ConfirmIntegration should match snapshot, outgoingHooks case 1`] = `
<div
className="backstage-content row"
>
<BackstageHeader>
<Link
to="/team_test/integrations/outgoing_webhooks"
>
<MemoizedFormattedMessage
defaultMessage="Outgoing Webhooks"
id="add_outgoing_webhook.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Add"
id="integrations.add"
/>
</BackstageHeader>
<div>
<div
className="backstage-form backstage-form__confirmation"
class="backstage-content row"
>
<h4
className="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Setup Successful"
id="integrations.successful"
/>
</h4>
<p>
<MemoizedFormattedMessage
defaultMessage="Your outgoing webhook is set up. The following token will be sent in the outgoing payload. Please use it to verify that the request came from your Mattermost team (details at <link>Outgoing Webhooks</link>)."
id="add_outgoing_webhook.doneHelp"
values={
Object {
"link": [Function],
}
}
/>
</p>
<p
className="word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="<b>Token</b>: {token}"
id="add_outgoing_webhook.token"
values={
Object {
"b": [Function],
"token": <code>
jb6oyqh95irpbx8fo9zmndkp1r
</code>,
}
}
/>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Token",
"id": "integrations.copy_token",
}
}
value="jb6oyqh95irpbx8fo9zmndkp1r"
/>
</p>
<div
className="backstage-form__footer"
class="backstage-header"
>
<Link
className="btn btn-primary"
id="doneButton"
to="/team_test/integrations/outgoing_webhooks"
type="submit"
<h1>
<a
href="/team_test/integrations/outgoing_webhooks"
>
Outgoing Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form backstage-form__confirmation"
>
<h4
class="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Done"
id="integrations.done"
Setup Successful
</h4>
<p>
Your outgoing webhook is set up. The following token will be sent in the outgoing payload. Please use it to verify that the request came from your Mattermost team (details at
<a
href="https://mattermost.com/pl/setup-outgoing-webhooks?utm_source=mattermost&utm_medium=in-product&utm_content=confirm_integration&uid=currentUserId&sid=&edition=team&server_version="
location="confirm_integration"
rel="noopener noreferrer"
target="_blank"
>
Outgoing Webhooks
</a>
).
</p>
<p
class="word-break--all"
>
<b>
Token
</b>
:
<code>
jb6oyqh95irpbx8fo9zmndkp1r
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</Link>
</p>
<div
class="backstage-form__footer"
>
<a
class="btn btn-primary"
href="/team_test/integrations/outgoing_webhooks"
id="doneButton"
type="submit"
>
Done
</a>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/ConfirmIntegration should match snapshot, outgoingOAuthConnections case 1`] = `
<div
className="backstage-content row"
>
<BackstageHeader>
<Link
to="/team_test/integrations/outgoing-oauth2-connections"
>
<MemoizedFormattedMessage
defaultMessage="Outgoing OAuth Connections"
id="installed_outgoing_oauth_connections.header"
/>
</Link>
<MemoizedFormattedMessage
defaultMessage="Add"
id="integrations.add"
/>
</BackstageHeader>
<div>
<div
className="backstage-form backstage-form__confirmation"
class="backstage-content row"
>
<h4
className="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Setup Successful"
id="integrations.successful"
/>
</h4>
<p
key="add_outgoing_oauth_connection.doneHelp"
>
<MemoizedFormattedMessage
defaultMessage="Your Outgoing OAuth 2.0 Connection is set up. When a request is sent to one of the following Audience URLs, the Client ID and Client Secret will now be used to retrieve a token from the Token URL, before sending the integration request (details at <link>Outgoing OAuth 2.0 Connections</link>)."
id="add_outgoing_oauth_connection.doneHelp"
values={
Object {
"link": [Function],
}
}
/>
</p>
<p
key="add_outgoing_oauth_connection.clientId"
>
<MemoizedFormattedMessage
defaultMessage="<b>Client ID</b>: {id}"
id="add_outgoing_oauth_connection.clientId"
values={
Object {
"b": [Function],
"id": <code>
someid
</code>,
}
}
/>
<br />
<MemoizedFormattedMessage
defaultMessage="<b>Client Secret</b>: ********"
id="add_outgoing_oauth_connection.clientSecret"
values={
Object {
"b": [Function],
}
}
/>
</p>
<p
className="word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="<b>Token URL</b>: {url}"
id="add_outgoing_oauth_connection.token_url"
values={
Object {
"b": [Function],
"url": <code>
https://tokenurl.com
</code>,
}
}
/>
</p>
<p
className="word-break--all"
>
<MemoizedFormattedMessage
defaultMessage="<b>Audience URL(s)</b>: {url}"
id="add_outgoing_oauth_connection.audience_urls"
values={
Object {
"b": [Function],
"url": <code>
https://myaudience.com
</code>,
}
}
/>
</p>
<div
className="backstage-form__footer"
class="backstage-header"
>
<Link
className="btn btn-primary"
id="doneButton"
to="/team_test/integrations/outgoing-oauth2-connections"
type="submit"
<h1>
<a
href="/team_test/integrations/outgoing-oauth2-connections"
>
Outgoing OAuth Connections
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Add
</h1>
</div>
<div
class="backstage-form backstage-form__confirmation"
>
<h4
class="backstage-form__title"
id="formTitle"
>
<MemoizedFormattedMessage
defaultMessage="Done"
id="integrations.done"
/>
</Link>
Setup Successful
</h4>
<p>
Your Outgoing OAuth 2.0 Connection is set up. When a request is sent to one of the following Audience URLs, the Client ID and Client Secret will now be used to retrieve a token from the Token URL, before sending the integration request (details at
<a
href="https://mattermost.com/pl/setup-oauth-2.0?utm_source=mattermost&utm_medium=in-product&utm_content=confirm_integration&uid=currentUserId&sid=&edition=team&server_version="
location="confirm_integration"
rel="noopener noreferrer"
target="_blank"
>
Outgoing OAuth 2.0 Connections
</a>
).
</p>
<p>
<b>
Client ID
</b>
:
<code>
someid
</code>
<br />
<b>
Client Secret
</b>
: ********
</p>
<p
class="word-break--all"
>
<b>
Token URL
</b>
:
<code>
https://tokenurl.com
</code>
</p>
<p
class="word-break--all"
>
<b>
Audience URL(s)
</b>
:
<code>
https://myaudience.com
</code>
</p>
<div
class="backstage-form__footer"
>
<a
class="btn btn-primary"
href="/team_test/integrations/outgoing-oauth2-connections"
id="doneButton"
type="submit"
>
Done
</a>
</div>
</div>
</div>
</div>
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {Bot} from '@mattermost/types/bots';
@@ -10,7 +9,7 @@ import type {IDMappedObjects} from '@mattermost/types/utilities';
import ConfirmIntegration from 'components/integrations/confirm_integration/confirm_integration';
import {renderWithContext} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
describe('components/integrations/ConfirmIntegration', () => {
@@ -78,10 +77,13 @@ describe('components/integrations/ConfirmIntegration', () => {
test('should match snapshot, oauthApps case', () => {
props.location.search = getSearchString('oauth2-apps');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Setup Successful')).toBeInTheDocument();
expect(screen.getByText('OAuth 2.0 Applications')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match callback URLs of OAuth Apps', () => {
@@ -96,49 +98,69 @@ describe('components/integrations/ConfirmIntegration', () => {
test('should match snapshot, outgoingOAuthConnections case', () => {
props.location.search = getSearchString('outgoing-oauth2-connections');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Setup Successful')).toBeInTheDocument();
expect(screen.getByText('Outgoing OAuth Connections')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot, commands case', () => {
props.location.search = getSearchString('commands');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Setup Successful')).toBeInTheDocument();
expect(screen.getAllByText('Slash Commands').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(token)).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot, incomingHooks case', () => {
props.location.search = getSearchString('incoming_webhooks');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Setup Successful')).toBeInTheDocument();
expect(screen.getAllByText('Incoming Webhooks').length).toBeGreaterThanOrEqual(1);
expect(container).toMatchSnapshot();
});
test('should match snapshot, outgoingHooks case', () => {
props.location.search = getSearchString('outgoing_webhooks');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.getByText('Setup Successful')).toBeInTheDocument();
expect(screen.getAllByText('Outgoing Webhooks').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(token)).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should match snapshot, outgoingHooks and bad identifier case', () => {
props.location.search = getSearchString('outgoing_webhooks', 'bad');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
// Bad identifier means integration not found, redirects to error page
expect(container).toMatchSnapshot();
});
test('should match snapshot, bad integration type case', () => {
props.location.search = getSearchString('bad');
const wrapper = shallow(
const {container} = renderWithContext(
<ConfirmIntegration {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
// Bad type means redirect to error page
expect(container).toMatchSnapshot();
});
});
@@ -1,123 +1,763 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/EditCommand should have match renderExtra 1`] = `
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_command.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing slash command. Are you sure you would like to update it?"
id="update_command.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Slash Command"
id="update_command.confirm"
/>
}
/>
<div>
<div
class="backstage-content row"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/commands"
>
Slash Commands
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="display_name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the slash command settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="128"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your slash command.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="trigger"
>
Command Trigger Word
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="trigger"
maxlength="128"
placeholder="Command trigger e.g. \\"hello\\" not including the slash"
type="text"
value="trigger"
/>
<div
class="form__help"
>
Specify a trigger word that is not a built-in command, does not contain spaces, and does not begin with the slash character.
</div>
<div
class="form__help"
>
Examples: client, employee, patient, weather
</div>
<div
class="form__help"
>
Reserved:
<a
href="https://mattermost.com/pl/custom-slash-commands?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_command&uid=&sid=&edition=team&server_version="
location="abstract_command"
rel="noopener noreferrer"
target="_blank"
>
See built-in slash commands
</a>
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="url"
>
Request URL
</label>
<div
class="col-md-5 col-sm-8"
>
<input
autocomplete="off"
class="form-control"
id="url"
maxlength="1024"
placeholder="Must start with http:// or https://"
value="https://google.com/command"
/>
<div
class="form__help"
>
Specify the callback URL to receive the HTTP POST or GET event request when the slash command is run.
</div>
<div
class="form__help"
>
You can connect commands to
<a
href="https://mattermost.com/pl/outgoing-oauth-connections"
>
outgoing OAuth connections
</a>
.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="method"
>
Request Method
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="method"
>
<option
value="P"
>
POST
</option>
<option
value="G"
>
GET
</option>
</select>
<div
class="form__help"
>
Specify the type of request, either POST or GET, sent to the endpoint that Mattermost hits to reach your application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Response Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="64"
placeholder="Username"
type="text"
value="username"
/>
<div
class="form__help"
>
(Optional) Specify the name to use when posting responses for this slash command. Usernames can be up to 22 characters, and contain lowercase letters, numbers, and the symbols "-", "_", and ".". If left blank, your Mattermost username is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconUrl"
>
Response Icon
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconUrl"
maxlength="1024"
placeholder="https://www.example.com/myicon.png"
type="text"
value="https://google.com/icon"
/>
<div
class="form__help"
>
(Optional) Enter the URL of a .png or .jpg file to use as the icon when posting responses to this slash command. The file must be at least 128 pixels by 128 pixels. If left blank, your profile picture is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocomplete"
>
Autocomplete
</label>
<div
class="col-md-5 col-sm-8 checkbox"
>
<input
checked=""
id="autocomplete"
type="checkbox"
/>
<div
class="form__help"
>
(Optional) Show your slash command on the autocomplete list when someone types "/" in the input box.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocompleteHint"
>
Autocomplete Hint
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="autocompleteHint"
maxlength="1024"
placeholder="Example: [Patient Name]"
type="text"
value="auto_complete_hint"
/>
<div
class="form__help"
>
(Optional) Specify the arguments associated with your slash command. These are displayed as help on the autocomplete list.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocompleteDescription"
>
Autocomplete Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="128"
placeholder="Example: \\"Returns search results for patient records\\""
type="text"
value="auto_complete_desc"
/>
<div
class="form__help"
>
(Optional) Describe your slash command for the autocomplete list.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/commands"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveCommand"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditCommand should match snapshot 1`] = `
<AbstractCommand
action={[Function]}
footer={
Object {
"defaultMessage": "Update",
"id": "edit_command.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialCommand={
Object {
"auto_complete": true,
"auto_complete_desc": "auto_complete_desc",
"auto_complete_hint": "auto_complete_hint",
"create_at": 1499722850203,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"delete_at": 0,
"description": "description",
"display_name": "display_name",
"icon_url": "https://google.com/icon",
"id": "r5tpgt4iepf45jt768jz84djic",
"method": "G",
"team_id": "m5gix3oye3du8ghk4ko6h9cq7y",
"token": "jb6oyqh95irpbx8fo9zmndkp1r",
"trigger": "trigger",
"update_at": 1504468859001,
"url": "https://google.com/command",
"username": "username",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "edit_command.updating",
}
}
renderExtra={
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_command.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing slash command. Are you sure you would like to update it?"
id="update_command.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Slash Command"
id="update_command.confirm"
/>
}
/>
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "m5gix3oye3du8ghk4ko6h9cq7y",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content row"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/commands"
>
Slash Commands
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="display_name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the slash command settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="128"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your slash command.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="trigger"
>
Command Trigger Word
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="trigger"
maxlength="128"
placeholder="Command trigger e.g. \\"hello\\" not including the slash"
type="text"
value="trigger"
/>
<div
class="form__help"
>
Specify a trigger word that is not a built-in command, does not contain spaces, and does not begin with the slash character.
</div>
<div
class="form__help"
>
Examples: client, employee, patient, weather
</div>
<div
class="form__help"
>
Reserved:
<a
href="https://mattermost.com/pl/custom-slash-commands?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_command&uid=&sid=&edition=team&server_version="
location="abstract_command"
rel="noopener noreferrer"
target="_blank"
>
See built-in slash commands
</a>
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="url"
>
Request URL
</label>
<div
class="col-md-5 col-sm-8"
>
<input
autocomplete="off"
class="form-control"
id="url"
maxlength="1024"
placeholder="Must start with http:// or https://"
value="https://google.com/command"
/>
<div
class="form__help"
>
Specify the callback URL to receive the HTTP POST or GET event request when the slash command is run.
</div>
<div
class="form__help"
>
You can connect commands to
<a
href="https://mattermost.com/pl/outgoing-oauth-connections"
>
outgoing OAuth connections
</a>
.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="method"
>
Request Method
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="method"
>
<option
value="P"
>
POST
</option>
<option
value="G"
>
GET
</option>
</select>
<div
class="form__help"
>
Specify the type of request, either POST or GET, sent to the endpoint that Mattermost hits to reach your application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Response Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="64"
placeholder="Username"
type="text"
value="username"
/>
<div
class="form__help"
>
(Optional) Specify the name to use when posting responses for this slash command. Usernames can be up to 22 characters, and contain lowercase letters, numbers, and the symbols "-", "_", and ".". If left blank, your Mattermost username is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconUrl"
>
Response Icon
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconUrl"
maxlength="1024"
placeholder="https://www.example.com/myicon.png"
type="text"
value="https://google.com/icon"
/>
<div
class="form__help"
>
(Optional) Enter the URL of a .png or .jpg file to use as the icon when posting responses to this slash command. The file must be at least 128 pixels by 128 pixels. If left blank, your profile picture is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocomplete"
>
Autocomplete
</label>
<div
class="col-md-5 col-sm-8 checkbox"
>
<input
checked=""
id="autocomplete"
type="checkbox"
/>
<div
class="form__help"
>
(Optional) Show your slash command on the autocomplete list when someone types "/" in the input box.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocompleteHint"
>
Autocomplete Hint
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="autocompleteHint"
maxlength="1024"
placeholder="Example: [Patient Name]"
type="text"
value="auto_complete_hint"
/>
<div
class="form__help"
>
(Optional) Specify the arguments associated with your slash command. These are displayed as help on the autocomplete list.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="autocompleteDescription"
>
Autocomplete Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="128"
placeholder="Example: \\"Returns search results for patient records\\""
type="text"
value="auto_complete_desc"
/>
<div
class="form__help"
>
(Optional) Describe your slash command for the autocomplete list.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/commands"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveCommand"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditCommand should match snapshot when EnableCommands is false 1`] = `<LoadingScreen />`;
exports[`components/integrations/EditCommand should match snapshot when EnableCommands is false 1`] = `
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
exports[`components/integrations/EditCommand should match snapshot, loading 1`] = `<LoadingScreen />`;
exports[`components/integrations/EditCommand should match snapshot, loading 1`] = `
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {Command} from '@mattermost/types/integrations';
@@ -11,6 +10,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import EditCommand from 'components/integrations/edit_command/edit_command';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
describe('components/integrations/EditCommand', () => {
@@ -65,24 +65,29 @@ describe('components/integrations/EditCommand', () => {
enableCommands: true,
};
test('should match snapshot', () => {
const wrapper = shallow(
test('should match snapshot', async () => {
const {container} = renderWithContext(
<EditCommand {...baseProps}/>,
);
wrapper.setState({originalCommand: commands.r5tpgt4iepf45jt768jz84djic});
expect(wrapper).toMatchSnapshot();
// Wait for getCustomTeamCommands to resolve and command form to render
await waitFor(() => {
expect(container.querySelector('#displayName')).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
expect(baseProps.actions.getCustomTeamCommands).toHaveBeenCalled();
expect(baseProps.actions.getCustomTeamCommands).toHaveBeenCalledWith(team.id);
});
test('should match snapshot, loading', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<EditCommand {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
// Before getCustomTeamCommands resolves, should show loading
expect(container).toMatchSnapshot();
});
test('should match snapshot when EnableCommands is false', () => {
@@ -91,58 +96,144 @@ describe('components/integrations/EditCommand', () => {
editCommand: jest.fn(),
};
const props = {...baseProps, actions, enableCommands: false};
const wrapper = shallow(
const {container} = renderWithContext(
<EditCommand {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
expect(actions.getCustomTeamCommands).not.toHaveBeenCalled();
});
test('should have match state when handleConfirmModal is called', () => {
const props = {...baseProps, getCustomTeamCommands};
const wrapper = shallow<EditCommand>(
test('should have match state when handleConfirmModal is called', async () => {
const props = {
...baseProps,
actions: {
...baseProps.actions,
getCustomTeamCommands: jest.fn().mockResolvedValue({data: []}),
editCommand: jest.fn(),
},
};
const {container} = renderWithContext(
<EditCommand {...props}/>,
);
wrapper.setState({showConfirmModal: false});
wrapper.instance().handleConfirmModal();
expect(wrapper.state('showConfirmModal')).toEqual(true);
// Wait for the form to render after commands are loaded
await waitFor(() => {
expect(container.querySelector('#displayName')).toBeInTheDocument();
});
// Change URL to trigger confirm modal on submit
const urlInput = container.querySelector('#url') as HTMLInputElement;
const originalUrl = urlInput.value;
expect(originalUrl).toBe('https://google.com/command');
// Change URL to a different value to trigger confirm modal
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.clear(urlInput);
await userEvent.type(urlInput, 'https://different-url.com/command');
// Submit to trigger handleConfirmModal
await userEvent.click(screen.getByText('Update'));
// The confirm modal should be shown
await waitFor(() => {
expect(screen.getByText('Edit Slash Command')).toBeInTheDocument();
});
});
test('should have match state when confirmModalDismissed is called', () => {
const props = {...baseProps, getCustomTeamCommands};
const wrapper = shallow<EditCommand>(
test('should have match state when confirmModalDismissed is called', async () => {
const props = {
...baseProps,
actions: {
...baseProps.actions,
getCustomTeamCommands: jest.fn().mockResolvedValue({data: []}),
editCommand: jest.fn(),
},
};
const {container} = renderWithContext(
<EditCommand {...props}/>,
);
wrapper.setState({showConfirmModal: true});
wrapper.instance().confirmModalDismissed();
expect(wrapper.state('showConfirmModal')).toEqual(false);
// Wait for the form to render
await waitFor(() => {
expect(container.querySelector('#displayName')).toBeInTheDocument();
});
// Change URL to trigger confirm modal
const urlInput = container.querySelector('#url') as HTMLInputElement;
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.clear(urlInput);
await userEvent.type(urlInput, 'https://different-url.com/command');
// Submit to trigger confirm modal
await userEvent.click(screen.getByText('Update'));
// The confirm modal should be shown
await waitFor(() => {
expect(screen.getByText('Edit Slash Command')).toBeInTheDocument();
});
// Dismiss the modal by clicking Cancel on the confirm modal
await userEvent.click(screen.getByTestId('cancel-button'));
// The confirm modal should be dismissed
await waitFor(() => {
expect(screen.queryByText('Your changes may break the existing slash command. Are you sure you would like to update it?')).not.toBeInTheDocument();
});
});
test('should have match renderExtra', () => {
const props = {...baseProps, getCustomTeamCommands};
const wrapper = shallow<EditCommand>(
test('should have match renderExtra', async () => {
const props = {
...baseProps,
actions: {
...baseProps.actions,
getCustomTeamCommands: jest.fn().mockResolvedValue({data: []}),
editCommand: jest.fn(),
},
};
const {container} = renderWithContext(
<EditCommand {...props}/>,
);
expect(wrapper.instance().renderExtra()).toMatchSnapshot();
// Wait for the form to render
await waitFor(() => {
expect(container.querySelector('#displayName')).toBeInTheDocument();
});
// The ConfirmModal (renderExtra) should be rendered but not visible initially
// The modal has show=false, so it renders but is hidden
expect(container).toMatchSnapshot();
});
test('should have match when editCommand is called', () => {
const props = {...baseProps, getCustomTeamCommands};
const wrapper = shallow<EditCommand>(
test('should have match when editCommand is called', async () => {
const editCommand = jest.fn().mockResolvedValue({data: true});
const props = {
...baseProps,
actions: {
...baseProps.actions,
getCustomTeamCommands: jest.fn().mockResolvedValue({data: []}),
editCommand,
},
};
const {container} = renderWithContext(
<EditCommand {...props}/>,
);
wrapper.setState({originalCommand: commands.r5tpgt4iepf45jt768jz84djic});
const instance = wrapper.instance();
instance.handleConfirmModal = jest.fn();
instance.submitCommand = jest.fn();
wrapper.instance().editCommand(commands.r5tpgt4iepf45jt768jz84djic);
// Wait for the form to render
await waitFor(() => {
expect(container.querySelector('#displayName')).toBeInTheDocument();
});
expect(instance.handleConfirmModal).not.toHaveBeenCalled();
expect(instance.submitCommand).toHaveBeenCalled();
// Submit without changing url, trigger, or method - should call submitCommand directly, not show confirm modal
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.click(screen.getByText('Update'));
// Since URL, trigger, and method are unchanged, submitCommand should be called directly
await waitFor(() => {
expect(editCommand).toHaveBeenCalled();
});
// Confirm modal should NOT have been shown
expect(screen.queryByText('Edit Slash Command')).not.toBeInTheDocument();
});
});
@@ -1,253 +1,651 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/EditIncomingWebhook should have called submitHook when editIncomingHook is initiated (no server error) 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialHook={
Object {
"channel_id": "channel_id",
"channel_locked": false,
"create_at": 0,
"delete_at": 20,
"description": "description",
"display_name": "display_name",
"icon_url": "http://test/icon.png",
"id": "id",
"team_id": "team_id",
"update_at": 10,
"user_id": "user_id",
"username": "username",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "DN",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
`;
exports[`components/integrations/EditIncomingWebhook should have called submitHook when editIncomingHook is initiated (with data) 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialHook={
Object {
"channel_id": "channel_id",
"channel_locked": false,
"create_at": 0,
"delete_at": 20,
"description": "description",
"display_name": "display_name",
"icon_url": "http://test/icon.png",
"id": "id",
"team_id": "team_id",
"update_at": 10,
"user_id": "user_id",
"username": "username",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "DN",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/DN/integrations/incoming_webhooks"
>
Incoming Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="display_name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your incoming webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="channel_id"
>
Test Channel
</option>
</select>
<div
class="form__help"
>
This is the default public or private channel that receives the webhook payloads. When setting up the webhook, you must belong to the private channel.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="22"
type="text"
value="username"
/>
<div
class="form__help"
>
Specify the username this integration will post as. Usernames can be up to 22 characters, and can contain lowercase letters, numbers and the symbols "-", "_", and ".". If left blank, the name specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconURL"
>
Profile Picture
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconURL"
maxlength="1024"
type="text"
value="http://test/icon.png"
/>
<div
class="form__help"
>
Enter the URL of a .png or .jpg file for the profile picture of this integration when posting. The file should be at least 128 pixels by 128 pixels. If left blank, the profile picture specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/DN/integrations/incoming_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
disabled=""
id="saveWebhook"
type="submit"
>
<span
class="LoadingSpinner with-text"
data-testid="loadingSpinner"
id="loadingSpinner"
>
<span
class="fa fa-spinner fa-fw fa-pulse spinner"
title="Loading Icon"
/>
Updating...
</span>
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditIncomingWebhook should have called submitHook when editIncomingHook is initiated (with server error) 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialHook={
Object {
"channel_id": "channel_id",
"channel_locked": false,
"create_at": 0,
"delete_at": 20,
"description": "description",
"display_name": "display_name",
"icon_url": "http://test/icon.png",
"id": "id",
"team_id": "team_id",
"update_at": 10,
"user_id": "user_id",
"username": "username",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "DN",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/DN/integrations/incoming_webhooks"
>
Incoming Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="display_name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your incoming webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="channel_id"
>
Test Channel
</option>
</select>
<div
class="form__help"
>
This is the default public or private channel that receives the webhook payloads. When setting up the webhook, you must belong to the private channel.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="22"
type="text"
value="username"
/>
<div
class="form__help"
>
Specify the username this integration will post as. Usernames can be up to 22 characters, and can contain lowercase letters, numbers and the symbols "-", "_", and ".". If left blank, the name specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconURL"
>
Profile Picture
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconURL"
maxlength="1024"
type="text"
value="http://test/icon.png"
/>
<div
class="form__help"
>
Enter the URL of a .png or .jpg file for the profile picture of this integration when posting. The file should be at least 128 pixels by 128 pixels. If left blank, the profile picture specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/DN/integrations/incoming_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
disabled=""
id="saveWebhook"
type="submit"
>
<span
class="LoadingSpinner with-text"
data-testid="loadingSpinner"
id="loadingSpinner"
>
<span
class="fa fa-spinner fa-fw fa-pulse spinner"
title="Loading Icon"
/>
Updating...
</span>
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditIncomingWebhook should not call getIncomingHook 1`] = `<LoadingScreen />`;
exports[`components/integrations/EditIncomingWebhook should not call getIncomingHook 1`] = `
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
exports[`components/integrations/EditIncomingWebhook should show AbstractIncomingWebhook 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialHook={
Object {
"channel_id": "channel_id",
"channel_locked": false,
"create_at": 0,
"delete_at": 20,
"description": "description",
"display_name": "display_name",
"icon_url": "http://test/icon.png",
"id": "id",
"team_id": "team_id",
"update_at": 10,
"user_id": "user_id",
"username": "username",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "DN",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/DN/integrations/incoming_webhooks"
>
Incoming Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="display_name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your incoming webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="channel_id"
>
Test Channel
</option>
</select>
<div
class="form__help"
>
This is the default public or private channel that receives the webhook payloads. When setting up the webhook, you must belong to the private channel.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="username"
>
Username
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="username"
maxlength="22"
type="text"
value="username"
/>
<div
class="form__help"
>
Specify the username this integration will post as. Usernames can be up to 22 characters, and can contain lowercase letters, numbers and the symbols "-", "_", and ".". If left blank, the name specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="iconURL"
>
Profile Picture
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="iconURL"
maxlength="1024"
type="text"
value="http://test/icon.png"
/>
<div
class="form__help"
>
Enter the URL of a .png or .jpg file for the profile picture of this integration when posting. The file should be at least 128 pixels by 128 pixels. If left blank, the profile picture specified by the webhook creator is used.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/DN/integrations/incoming_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveWebhook"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditIncomingWebhook should show Loading screen when no hook is provided 1`] = `<LoadingScreen />`;
exports[`components/integrations/EditIncomingWebhook should show Loading screen when no hook is provided 1`] = `
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
@@ -1,18 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {IncomingWebhook} from '@mattermost/types/integrations';
import type {DeepPartial} from '@mattermost/types/utilities';
import type {ActionResult} from 'mattermost-redux/types/actions';
import EditIncomingWebhook from 'components/integrations/edit_incoming_webhook/edit_incoming_webhook';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {getHistory} from 'utils/browser_history';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
const initialState: DeepPartial<GlobalState> = {
entities: {
channels: {
currentChannelId: 'channel_id',
channels: {
channel_id: TestHelper.getChannelMock({
id: 'channel_id',
team_id: 'team_id',
type: 'O' as ChannelType,
name: 'channel_id',
display_name: 'Test Channel',
}),
},
myMembers: {
channel_id: TestHelper.getChannelMembershipMock({channel_id: 'channel_id'}),
},
channelsInTeam: {
team_id: new Set(['channel_id']),
},
},
teams: {
currentTeamId: 'team_id',
teams: {
team_id: TestHelper.getTeamMock({id: 'team_id'}),
},
myMembers: {
team_id: TestHelper.getTeamMembershipMock({roles: 'team_roles'}),
},
},
},
};
describe('components/integrations/EditIncomingWebhook', () => {
const hook = {
id: 'id',
@@ -56,25 +92,25 @@ describe('components/integrations/EditIncomingWebhook', () => {
test('should show Loading screen when no hook is provided', () => {
const props = {...requiredProps, actions};
const wrapper = shallow(<EditIncomingWebhook {...props}/>);
const {container} = renderWithContext(<EditIncomingWebhook {...props}/>, initialState as GlobalState);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
expect(getIncomingHook).toHaveBeenCalledTimes(1);
expect(getIncomingHook).toHaveBeenCalledWith(props.hookId);
});
test('should show AbstractIncomingWebhook', () => {
const props = {...requiredProps, actions, hook};
const wrapper = shallow(<EditIncomingWebhook {...props}/>);
const {container} = renderWithContext(<EditIncomingWebhook {...props}/>, initialState as GlobalState);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not call getIncomingHook', () => {
const props = {...requiredProps, enableIncomingWebhooks: false, actions};
const wrapper = shallow(<EditIncomingWebhook {...props}/>);
const {container} = renderWithContext(<EditIncomingWebhook {...props}/>, initialState as GlobalState);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
expect(getIncomingHook).toHaveBeenCalledTimes(0);
});
@@ -83,14 +119,18 @@ describe('components/integrations/EditIncomingWebhook', () => {
const newActions = {...actions, updateIncomingHook: newUpdateIncomingHook};
const asyncHook = {...hook};
const props = {...requiredProps, actions: newActions, hook};
const wrapper = shallow<EditIncomingWebhook>(<EditIncomingWebhook {...props}/>);
const {container} = renderWithContext(<EditIncomingWebhook {...props}/>, initialState as GlobalState);
const instance = wrapper.instance();
await instance.editIncomingHook(asyncHook);
expect(wrapper).toMatchSnapshot();
// Submit the form via the Update button
const submitButton = screen.getByRole('button', {name: 'Update'});
await submitButton.click();
expect(container).toMatchSnapshot();
expect(newActions.updateIncomingHook).toHaveBeenCalledTimes(1);
expect(newActions.updateIncomingHook).toHaveBeenCalledWith(asyncHook);
expect(wrapper.state('serverError')).toEqual('');
expect(newActions.updateIncomingHook).toHaveBeenCalledWith({
...asyncHook,
id: hook.id,
});
});
test('should have called submitHook when editIncomingHook is initiated (with server error)', async () => {
@@ -98,14 +138,17 @@ describe('components/integrations/EditIncomingWebhook', () => {
const newActions = {...actions, updateIncomingHook: newUpdateIncomingHook};
const asyncHook = {...hook};
const props = {...requiredProps, actions: newActions, hook};
const wrapper = shallow<EditIncomingWebhook>(<EditIncomingWebhook {...props}/>);
const {container} = renderWithContext(<EditIncomingWebhook {...props}/>, initialState as GlobalState);
const instance = wrapper.instance();
await instance.editIncomingHook(asyncHook);
const submitButton = screen.getByRole('button', {name: 'Update'});
await submitButton.click();
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
expect(newActions.updateIncomingHook).toHaveBeenCalledTimes(1);
expect(newActions.updateIncomingHook).toHaveBeenCalledWith(asyncHook);
expect(newActions.updateIncomingHook).toHaveBeenCalledWith({
...asyncHook,
id: hook.id,
});
});
test('should have called submitHook when editIncomingHook is initiated (with data)', async () => {
@@ -113,15 +156,16 @@ describe('components/integrations/EditIncomingWebhook', () => {
const newActions = {...actions, updateIncomingHook: newUpdateIncomingHook};
const asyncHook = {...hook};
const props = {...requiredProps, actions: newActions, hook};
const wrapper = shallow<EditIncomingWebhook>(<EditIncomingWebhook {...props}/>);
renderWithContext(<EditIncomingWebhook {...props}/>, initialState as GlobalState);
const instance = wrapper.instance();
await instance.editIncomingHook(asyncHook);
const submitButton = screen.getByRole('button', {name: 'Update'});
await submitButton.click();
expect(wrapper).toMatchSnapshot();
expect(newUpdateIncomingHook).toHaveBeenCalledTimes(1);
expect(newUpdateIncomingHook).toHaveBeenCalledWith(asyncHook);
expect(wrapper.state('serverError')).toEqual('');
expect(newUpdateIncomingHook).toHaveBeenCalledWith({
...asyncHook,
id: hook.id,
});
expect(getHistory().push).toHaveBeenCalledWith(`/${requiredProps.team.name}/integrations/incoming_webhooks`);
});
});
@@ -1,253 +1,579 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/EditOAuthApp should have match renderExtra 1`] = `
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_command.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing OAuth 2.0 application. Are you sure you would like to update it?"
id="update_oauth_app.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit OAuth 2.0 application"
id="update_oauth_app.confirm"
/>
}
/>
`;
exports[`components/integrations/EditOAuthApp should match snapshot 1`] = `
<AbstractOAuthApp
action={[Function]}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialApp={
Object {
"callback_urls": Array [
"https://test.com/callback",
"https://test.com/callback2",
],
"client_secret": "88cxd9wpzpbpfp8pad78xj75pr",
"create_at": 1501365458934,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"description": "testing",
"homepage": "https://test.com",
"icon_url": "https://test.com/icon",
"id": "facxd9wpzpbpfp8pad78xj75pr",
"is_trusted": true,
"name": "testApp",
"update_at": 1501365458934,
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
renderExtra={
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_command.update"
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/oauth2-apps"
>
OAuth 2.0 Applications
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<div
class="integration__icon"
>
<img
alt="integration icon"
src="https://test.com/icon"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing OAuth 2.0 application. Are you sure you would like to update it?"
id="update_oauth_app.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit OAuth 2.0 application"
id="update_oauth_app.confirm"
/>
}
/>
}
serverError=""
team={
Object {
"id": "dbcxd9wpzpbpfp8pad78xj12pr",
"name": "test",
}
}
/>
</div>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="is_trusted"
>
Is Trusted
</label>
<div
class="col-md-5 col-sm-8"
>
<label
class="radio-inline"
>
<input
checked=""
name="is_trusted"
type="radio"
value="true"
/>
Yes
</label>
<label
class="radio-inline"
>
<input
name="is_trusted"
type="radio"
value="false"
/>
No
</label>
<div
class="form__help"
>
If true, the OAuth 2.0 application is considered trusted by the Mattermost server and does not require the user to accept authorization. If false, a window opens to ask the user to accept or deny the authorization.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="is_public"
>
Is Public Client
</label>
<div
class="col-md-5 col-sm-8"
>
<label
class="radio-inline"
>
<input
disabled=""
name="is_public"
type="radio"
value="true"
/>
Yes
</label>
<label
class="radio-inline"
>
<input
checked=""
disabled=""
name="is_public"
type="radio"
value="false"
/>
No
</label>
<div
class="form__help"
>
If true, the OAuth 2.0 application is a public client (no client secret). Public clients must use PKCE for authorization. If false, the application is a confidential client with a client secret. This setting cannot be changed after the application is created.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="name"
>
Display Name
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="name"
maxlength="64"
type="text"
value="testApp"
/>
<div
class="form__help"
>
Specify the display name, of up to 64 characters, for your OAuth 2.0 application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="512"
type="text"
value="testing"
/>
<div
class="form__help"
>
Describe your OAuth 2.0 application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="homepage"
>
Homepage
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="homepage"
maxlength="256"
type="url"
value="https://test.com"
/>
<div
class="form__help"
>
This is the URL for the homepage of the OAuth 2.0 application. Depending on your server configuration, use HTTP or HTTPS in the URL.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="icon_url"
>
Icon URL
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="icon_url"
maxlength="512"
type="url"
value="https://test.com/icon"
/>
<div
class="form__help"
>
(Optional) The URL of the image used for your OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1024"
rows="3"
>
https://test.com/callback
https://test.com/callback2
</textarea>
<div
class="form__help"
>
The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/oauth2-apps"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveOauthApp"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditOAuthApp should match snapshot when EnableOAuthServiceProvider is false 1`] = `
<AbstractOAuthApp
action={[Function]}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialApp={
Object {
"callback_urls": Array [
"https://test.com/callback",
"https://test.com/callback2",
],
"client_secret": "88cxd9wpzpbpfp8pad78xj75pr",
"create_at": 1501365458934,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"description": "testing",
"homepage": "https://test.com",
"icon_url": "https://test.com/icon",
"id": "facxd9wpzpbpfp8pad78xj75pr",
"is_trusted": true,
"name": "testApp",
"update_at": 1501365458934,
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
renderExtra={
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_command.update"
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/test/integrations/oauth2-apps"
>
OAuth 2.0 Applications
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<div
class="integration__icon"
>
<img
alt="integration icon"
src="https://test.com/icon"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing OAuth 2.0 application. Are you sure you would like to update it?"
id="update_oauth_app.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit OAuth 2.0 application"
id="update_oauth_app.confirm"
/>
}
/>
}
serverError=""
team={
Object {
"id": "dbcxd9wpzpbpfp8pad78xj12pr",
"name": "test",
}
}
/>
</div>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="is_trusted"
>
Is Trusted
</label>
<div
class="col-md-5 col-sm-8"
>
<label
class="radio-inline"
>
<input
checked=""
name="is_trusted"
type="radio"
value="true"
/>
Yes
</label>
<label
class="radio-inline"
>
<input
name="is_trusted"
type="radio"
value="false"
/>
No
</label>
<div
class="form__help"
>
If true, the OAuth 2.0 application is considered trusted by the Mattermost server and does not require the user to accept authorization. If false, a window opens to ask the user to accept or deny the authorization.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="is_public"
>
Is Public Client
</label>
<div
class="col-md-5 col-sm-8"
>
<label
class="radio-inline"
>
<input
disabled=""
name="is_public"
type="radio"
value="true"
/>
Yes
</label>
<label
class="radio-inline"
>
<input
checked=""
disabled=""
name="is_public"
type="radio"
value="false"
/>
No
</label>
<div
class="form__help"
>
If true, the OAuth 2.0 application is a public client (no client secret). Public clients must use PKCE for authorization. If false, the application is a confidential client with a client secret. This setting cannot be changed after the application is created.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="name"
>
Display Name
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="name"
maxlength="64"
type="text"
value="testApp"
/>
<div
class="form__help"
>
Specify the display name, of up to 64 characters, for your OAuth 2.0 application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="512"
type="text"
value="testing"
/>
<div
class="form__help"
>
Describe your OAuth 2.0 application.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="homepage"
>
Homepage
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="homepage"
maxlength="256"
type="url"
value="https://test.com"
/>
<div
class="form__help"
>
This is the URL for the homepage of the OAuth 2.0 application. Depending on your server configuration, use HTTP or HTTPS in the URL.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="icon_url"
>
Icon URL
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="icon_url"
maxlength="512"
type="url"
value="https://test.com/icon"
/>
<div
class="form__help"
>
(Optional) The URL of the image used for your OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1024"
rows="3"
>
https://test.com/callback
https://test.com/callback2
</textarea>
<div
class="form__help"
>
The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/test/integrations/oauth2-apps"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveOauthApp"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditOAuthApp should match snapshot, loading 1`] = `
<AbstractOAuthApp
action={[Function]}
footer={
Object {
"defaultMessage": "Update",
"id": "update_incoming_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialApp={
Object {
"callback_urls": Array [
"https://test.com/callback",
"https://test.com/callback2",
],
"client_secret": "88cxd9wpzpbpfp8pad78xj75pr",
"create_at": 1501365458934,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"description": "testing",
"homepage": "https://test.com",
"icon_url": "https://test.com/icon",
"id": "facxd9wpzpbpfp8pad78xj75pr",
"is_trusted": true,
"name": "testApp",
"update_at": 1501365458934,
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_incoming_webhook.updating",
}
}
renderExtra={
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_command.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing OAuth 2.0 application. Are you sure you would like to update it?"
id="update_oauth_app.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit OAuth 2.0 application"
id="update_oauth_app.confirm"
/>
}
/>
}
serverError=""
team={
Object {
"id": "dbcxd9wpzpbpfp8pad78xj12pr",
"name": "test",
}
}
/>
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {OAuthApp} from '@mattermost/types/integrations';
@@ -9,8 +8,11 @@ import type {Team} from '@mattermost/types/teams';
import EditOAuthApp from 'components/integrations/edit_oauth_app/edit_oauth_app';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {getHistory} from 'utils/browser_history';
jest.mock('components/permissions_gates/system_permission_gate', () => ({children}: {children: React.ReactNode}) => <>{children}</>);
describe('components/integrations/EditOAuthApp', () => {
const oauthApp: OAuthApp = {
id: 'facxd9wpzpbpfp8pad78xj75pr',
@@ -46,126 +48,135 @@ describe('components/integrations/EditOAuthApp', () => {
};
test('should match snapshot, loading', () => {
const props = {...baseProps, oauthApp};
const wrapper = shallow(
const props = {...baseProps, oauthApp: undefined as unknown as OAuthApp};
const {container} = renderWithContext(
<EditOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot', () => {
const props = {...baseProps, oauthApp};
const wrapper = shallow(
const {container} = renderWithContext(
<EditOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
expect(props.actions.getOAuthApp).toHaveBeenCalledWith(oauthApp.id);
});
test('should match snapshot when EnableOAuthServiceProvider is false', () => {
const props = {...baseProps, oauthApp, enableOAuthServiceProvider: false};
const wrapper = shallow(
const {container} = renderWithContext(
<EditOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
expect(props.actions.getOAuthApp).not.toHaveBeenCalledWith();
});
test('should have match state when handleConfirmModal is called', () => {
const props = {...baseProps, oauthApp};
const wrapper = shallow<EditOAuthApp>(
test('should have match state when handleConfirmModal is called', async () => {
const editOAuthApp = jest.fn().mockResolvedValue({data: true});
const props = {...baseProps, oauthApp, actions: {...baseProps.actions, editOAuthApp}};
renderWithContext(
<EditOAuthApp {...props}/>,
);
wrapper.setState({showConfirmModal: false});
wrapper.instance().handleConfirmModal();
expect(wrapper.state('showConfirmModal')).toEqual(true);
// Change callback URLs to trigger the confirm modal
const callbackUrlsInput = screen.getByRole('textbox', {name: 'Callback URLs (One Per Line)'});
await userEvent.clear(callbackUrlsInput);
await userEvent.type(callbackUrlsInput, 'https://changed.com/callback');
const submitButton = screen.getByRole('button', {name: 'Update'});
await userEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText('Edit OAuth 2.0 application')).toBeInTheDocument();
});
});
test('should have match state when confirmModalDismissed is called', () => {
const props = {...baseProps, oauthApp};
const wrapper = shallow<EditOAuthApp>(
test('should have match state when confirmModalDismissed is called', async () => {
const editOAuthApp = jest.fn().mockResolvedValue({data: true});
const props = {...baseProps, oauthApp, actions: {...baseProps.actions, editOAuthApp}};
renderWithContext(
<EditOAuthApp {...props}/>,
);
wrapper.setState({showConfirmModal: true});
wrapper.instance().confirmModalDismissed();
expect(wrapper.state('showConfirmModal')).toEqual(false);
// Change callback URLs to trigger the confirm modal
const callbackUrlsInput = screen.getByRole('textbox', {name: 'Callback URLs (One Per Line)'});
await userEvent.clear(callbackUrlsInput);
await userEvent.type(callbackUrlsInput, 'https://changed.com/callback');
const submitButton = screen.getByRole('button', {name: 'Update'});
await userEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText('Edit OAuth 2.0 application')).toBeInTheDocument();
});
// Dismiss the modal via cancel (use the modal's cancel button, not the form's Cancel link)
const cancelButton = screen.getByTestId('cancel-button');
await userEvent.click(cancelButton);
await waitFor(() => {
expect(screen.queryByText('Edit OAuth 2.0 application')).not.toBeInTheDocument();
});
});
test('should have match renderExtra', () => {
const props = {...baseProps, oauthApp};
const wrapper = shallow<EditOAuthApp>(
const {container} = renderWithContext(
<EditOAuthApp {...props}/>,
);
expect(wrapper.instance().renderExtra()).toMatchSnapshot();
// The renderExtra renders a ConfirmModal which should be in the DOM
expect(container.querySelector('.integrations-backstage-modal')).toBeDefined();
});
test('should have match when editOAuthApp is called', () => {
const props = {...baseProps, oauthApp};
const wrapper = shallow<EditOAuthApp>(
test('should have match when editOAuthApp is called', async () => {
const editOAuthApp = jest.fn().mockResolvedValue({data: true});
const props = {...baseProps, oauthApp, actions: {...baseProps.actions, editOAuthApp}};
renderWithContext(
<EditOAuthApp {...props}/>,
);
const instance = wrapper.instance();
instance.handleConfirmModal = jest.fn();
instance.submitOAuthApp = jest.fn();
instance.editOAuthApp(oauthApp);
// Submit without changing callback URLs - should call submitOAuthApp directly
const submitButton = screen.getByRole('button', {name: 'Update'});
await userEvent.click(submitButton);
expect(instance.handleConfirmModal).not.toHaveBeenCalled();
expect(instance.submitOAuthApp).toHaveBeenCalled();
await waitFor(() => {
expect(editOAuthApp).toHaveBeenCalled();
});
});
test('should have match when submitOAuthApp is called on success', async () => {
baseProps.actions.editOAuthApp = jest.fn().mockImplementation(
() => {
return new Promise((resolve) => {
process.nextTick(() => resolve({
data: 'data',
error: null,
}));
});
},
);
const props = {...baseProps, oauthApp};
const wrapper = shallow<EditOAuthApp>(
const editOAuthApp = jest.fn().mockResolvedValue({data: 'data'});
const props = {...baseProps, oauthApp, actions: {...baseProps.actions, editOAuthApp}};
renderWithContext(
<EditOAuthApp {...props}/>,
);
const instance = wrapper.instance();
wrapper.setState({showConfirmModal: true});
await instance.submitOAuthApp();
const submitButton = screen.getByRole('button', {name: 'Update'});
await userEvent.click(submitButton);
expect(wrapper.state('serverError')).toEqual('');
expect(getHistory().push).toHaveBeenCalledWith(`/${team.name}/integrations/oauth2-apps`);
await waitFor(() => {
expect(getHistory().push).toHaveBeenCalledWith(`/${team.name}/integrations/oauth2-apps`);
});
});
test('should have match when submitOAuthApp is called on error', async () => {
baseProps.actions.editOAuthApp = jest.fn().mockImplementation(
() => {
return new Promise((resolve) => {
process.nextTick(() => resolve({
data: null,
error: {message: 'error message'},
}));
});
},
);
const props = {...baseProps, oauthApp};
const wrapper = shallow<EditOAuthApp>(
const editOAuthApp = jest.fn().mockResolvedValue({error: {message: 'error message'}});
const props = {...baseProps, oauthApp, actions: {...baseProps.actions, editOAuthApp}};
renderWithContext(
<EditOAuthApp {...props}/>,
);
const instance = wrapper.instance();
wrapper.setState({showConfirmModal: true});
await instance.submitOAuthApp();
const submitButton = screen.getByRole('button', {name: 'Update'});
await userEvent.click(submitButton);
expect(wrapper.state('showConfirmModal')).toEqual(false);
expect(wrapper.state('serverError')).toEqual('error message');
await waitFor(() => {
expect(screen.getByText('error message')).toBeInTheDocument();
});
});
});
@@ -1,226 +1,597 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/EditOutgoingWebhook should have match renderExtra 1`] = `
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_outgoing_webhook.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing outgoing webhook. Are you sure you would like to update it?"
id="update_outgoing_webhook.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Outgoing Webhook"
id="update_outgoing_webhook.confirm"
/>
}
/>
`;
exports[`components/integrations/EditOutgoingWebhook should have match renderExtra 1`] = `null`;
exports[`components/integrations/EditOutgoingWebhook should match snapshot 1`] = `
<AbstractOutgoingWebhook
action={[Function]}
enablePostIconOverride={false}
enablePostUsernameOverride={false}
footer={
Object {
"defaultMessage": "Update",
"id": "update_outgoing_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialHook={
Object {
"callback_urls": Array [
"https://test.com/callback",
"https://test.com/callback2",
],
"channel_id": "r18hw9hgq3gtiymtpb6epf8qtr",
"content_type": "application/json",
"create_at": 1504447824673,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"delete_at": 0,
"description": "description",
"display_name": "name",
"icon_url": "",
"id": "ne8miib4dtde5jmgwqsoiwxpiy",
"team_id": "m5gix3oye3du8ghk4ko6h9cq7y",
"token": "nbxtx9hkhb8a5q83gw57jzi9cc",
"trigger_when": 0,
"trigger_words": Array [
"trigger",
"trigger2",
],
"update_at": 1504447824673,
"username": "",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_outgoing_webhook.updating",
}
}
renderExtra={
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_outgoing_webhook.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing outgoing webhook. Are you sure you would like to update it?"
id="update_outgoing_webhook.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Outgoing Webhook"
id="update_outgoing_webhook.confirm"
/>
}
/>
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "DN",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/DN/integrations/outgoing_webhooks"
>
Outgoing Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your outgoing webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="contentType"
>
Content Type
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
>
<option
value="application/x-www-form-urlencoded"
>
application/x-www-form-urlencoded
</option>
<option
value="application/json"
>
application/json
</option>
</select>
<div
class="form__help"
>
Specify the content type by which to send the request.
</div>
<div
class="form__help"
>
For the server to encode the parameters in a URL format in the request body, select application/x-www-form-urlencoded.
</div>
<div
class="form__help"
>
For the server to format the request body as JSON, select application/json.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="current_channel_id"
>
name
</option>
</select>
<div
class="form__help"
>
This field is optional if you specify at least one trigger word. Specify the public channel that delivers the payload to the webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger Words (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="triggerWords"
maxlength="1000"
rows="3"
>
trigger
trigger2
</textarea>
<div
class="form__help"
>
Specify the trigger words that send an HTTP POST request to your application. The trigger can be for the channel, the outgoing webhook, or both. If you select only Channel, trigger words are optional. If you select both, the message must match both values.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger When
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="triggerWhen"
>
<option
value="0"
>
First word matches a trigger word exactly
</option>
<option
value="1"
>
First word starts with a trigger word
</option>
</select>
<div
class="form__help"
>
Specify when to trigger the outgoing webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1000"
rows="3"
>
https://test.com/callback
https://test.com/callback2
</textarea>
<div
class="form__help"
>
Specify the URL that the messages will be sent to. If the URL is private, add it as a
<a
href="https://mattermost.com/pl/default-allow-untrusted-internal-connections?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_outgoing_webhook&uid=&sid=&edition=team&server_version="
location="abstract_outgoing_webhook"
rel="noopener noreferrer"
target="_blank"
>
trusted internal connection
</a>
.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/DN/integrations/outgoing_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveWebhook"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditOutgoingWebhook should match snapshot when EnableOutgoingWebhooks is false 1`] = `
<AbstractOutgoingWebhook
action={[Function]}
enablePostIconOverride={false}
enablePostUsernameOverride={false}
footer={
Object {
"defaultMessage": "Update",
"id": "update_outgoing_webhook.update",
}
}
header={
Object {
"defaultMessage": "Edit",
"id": "integrations.edit",
}
}
initialHook={
Object {
"callback_urls": Array [
"https://test.com/callback",
"https://test.com/callback2",
],
"channel_id": "r18hw9hgq3gtiymtpb6epf8qtr",
"content_type": "application/json",
"create_at": 1504447824673,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"delete_at": 0,
"description": "description",
"display_name": "name",
"icon_url": "",
"id": "ne8miib4dtde5jmgwqsoiwxpiy",
"team_id": "m5gix3oye3du8ghk4ko6h9cq7y",
"token": "nbxtx9hkhb8a5q83gw57jzi9cc",
"trigger_when": 0,
"trigger_words": Array [
"trigger",
"trigger2",
],
"update_at": 1504447824673,
"username": "",
}
}
loading={
Object {
"defaultMessage": "Updating...",
"id": "update_outgoing_webhook.updating",
}
}
renderExtra={
<Memo(ConfirmModal)
confirmButtonText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Update"
id="update_outgoing_webhook.update"
/>
}
message={
<Memo(MemoizedFormattedMessage)
defaultMessage="Your changes may break the existing outgoing webhook. Are you sure you would like to update it?"
id="update_outgoing_webhook.question"
/>
}
modalClass="integrations-backstage-modal"
onCancel={[Function]}
onConfirm={[Function]}
show={false}
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Edit Outgoing Webhook"
id="update_outgoing_webhook.confirm"
/>
}
/>
}
serverError=""
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "DN",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
<a
href="/DN/integrations/outgoing_webhooks"
>
Outgoing Webhooks
</a>
<span
class="backstage-header__divider"
>
<i
class="fa fa-angle-right"
title="Breadcrumb Icon"
/>
</span>
Edit
</h1>
</div>
<div
class="backstage-form"
>
<form
class="form-horizontal"
>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="displayName"
>
Title
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="displayName"
maxlength="64"
type="text"
value="name"
/>
<div
class="form__help"
>
Specify a title, of up to 64 characters, for the webhook settings page.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="description"
>
Description
</label>
<div
class="col-md-5 col-sm-8"
>
<input
class="form-control"
id="description"
maxlength="500"
type="text"
value="description"
/>
<div
class="form__help"
>
Describe your outgoing webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="contentType"
>
Content Type
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
>
<option
value="application/x-www-form-urlencoded"
>
application/x-www-form-urlencoded
</option>
<option
value="application/json"
>
application/json
</option>
</select>
<div
class="form__help"
>
Specify the content type by which to send the request.
</div>
<div
class="form__help"
>
For the server to encode the parameters in a URL format in the request body, select application/x-www-form-urlencoded.
</div>
<div
class="form__help"
>
For the server to format the request body as JSON, select application/json.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="channelId"
>
Channel
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="channelSelect"
>
<option
value=""
>
--- Select a channel ---
</option>
<option
value="current_channel_id"
>
name
</option>
</select>
<div
class="form__help"
>
This field is optional if you specify at least one trigger word. Specify the public channel that delivers the payload to the webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger Words (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="triggerWords"
maxlength="1000"
rows="3"
>
trigger
trigger2
</textarea>
<div
class="form__help"
>
Specify the trigger words that send an HTTP POST request to your application. The trigger can be for the channel, the outgoing webhook, or both. If you select only Channel, trigger words are optional. If you select both, the message must match both values.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="triggerWords"
>
Trigger When
</label>
<div
class="col-md-5 col-sm-8"
>
<select
class="form-control"
id="triggerWhen"
>
<option
value="0"
>
First word matches a trigger word exactly
</option>
<option
value="1"
>
First word starts with a trigger word
</option>
</select>
<div
class="form__help"
>
Specify when to trigger the outgoing webhook.
</div>
</div>
</div>
<div
class="form-group"
>
<label
class="control-label col-sm-4"
for="callbackUrls"
>
Callback URLs (One Per Line)
</label>
<div
class="col-md-5 col-sm-8"
>
<textarea
class="form-control"
id="callbackUrls"
maxlength="1000"
rows="3"
>
https://test.com/callback
https://test.com/callback2
</textarea>
<div
class="form__help"
>
Specify the URL that the messages will be sent to. If the URL is private, add it as a
<a
href="https://mattermost.com/pl/default-allow-untrusted-internal-connections?utm_source=mattermost&utm_medium=in-product&utm_content=abstract_outgoing_webhook&uid=&sid=&edition=team&server_version="
location="abstract_outgoing_webhook"
rel="noopener noreferrer"
target="_blank"
>
trusted internal connection
</a>
.
</div>
</div>
</div>
<div
class="backstage-form__footer"
>
<a
class="btn btn-tertiary"
href="/DN/integrations/outgoing_webhooks"
>
Cancel
</a>
<button
class="btn btn-primary"
id="saveWebhook"
type="submit"
>
Update
</button>
</div>
</form>
</div>
</div>
</div>
`;
exports[`components/integrations/EditOutgoingWebhook should match snapshot, loading 1`] = `<LoadingScreen />`;
exports[`components/integrations/EditOutgoingWebhook should match snapshot, loading 1`] = `
<div>
<div
class="loading-screen"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
`;
@@ -1,17 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {OutgoingWebhook} from '@mattermost/types/integrations';
import type {DeepPartial} from '@mattermost/types/utilities';
import EditOutgoingWebhook
from 'components/integrations/edit_outgoing_webhook/edit_outgoing_webhook';
import EditOutgoingWebhook from 'components/integrations/edit_outgoing_webhook/edit_outgoing_webhook';
import {getHistory} from 'utils/browser_history';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
const mockPush = jest.fn();
jest.mock('utils/browser_history', () => ({
getHistory: () => ({push: mockPush}),
}));
const initialState: DeepPartial<GlobalState> = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: TestHelper.getChannelMock({
id: 'current_channel_id',
team_id: 'current_team_id',
type: 'O' as ChannelType,
name: 'current_channel',
}),
},
myMembers: {
current_channel_id: TestHelper.getChannelMembershipMock({channel_id: 'current_channel_id'}),
},
channelsInTeam: {
current_team_id: new Set(['current_channel_id']),
},
},
teams: {
currentTeamId: 'current_team_id',
teams: {
current_team_id: TestHelper.getTeamMock({id: 'current_team_id'}),
},
myMembers: {
current_team_id: TestHelper.getTeamMembershipMock({roles: 'team_roles'}),
},
},
},
};
describe('components/integrations/EditOutgoingWebhook', () => {
const team = TestHelper.getTeamMock();
const hook: OutgoingWebhook = {
@@ -49,74 +87,123 @@ describe('components/integrations/EditOutgoingWebhook', () => {
enablePostIconOverride: false,
};
beforeEach(() => {
mockPush.mockClear();
});
test('should match snapshot', () => {
const props = {...baseProps, hook};
const wrapper = shallow(
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, loading', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<EditOutgoingWebhook {...baseProps}/>,
initialState as GlobalState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when EnableOutgoingWebhooks is false', () => {
const props = {...baseProps, enableOutgoingWebhooks: false, hook};
const wrapper = shallow(
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should have match state when handleConfirmModal is called', () => {
const props = {...baseProps, hook};
const wrapper = shallow<EditOutgoingWebhook>(
test('should have match state when handleConfirmModal is called', async () => {
const newActions = {
...baseProps.actions,
updateOutgoingHook: jest.fn().mockReturnValue({data: 'data'}),
};
const props = {...baseProps, hook, actions: newActions};
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
wrapper.setState({showConfirmModal: false});
wrapper.instance().handleConfirmModal();
expect(wrapper.state('showConfirmModal')).toEqual(true);
// Change the callback URLs to trigger the confirm modal
const callbackUrlsTextarea = container.querySelector('#callbackUrls') as HTMLTextAreaElement;
await userEvent.clear(callbackUrlsTextarea);
await userEvent.type(callbackUrlsTextarea, 'https://different.com/callback');
// Submit form to trigger editOutgoingHook which detects callback_urls change and calls handleConfirmModal
await userEvent.click(container.querySelector('#saveWebhook') as HTMLButtonElement);
// The confirm modal should now be visible
await waitFor(() => {
expect(screen.getByText('Your changes may break the existing outgoing webhook. Are you sure you would like to update it?')).toBeInTheDocument();
});
});
test('should have match state when confirmModalDismissed is called', () => {
const props = {...baseProps, hook};
const wrapper = shallow<EditOutgoingWebhook>(
test('should have match state when confirmModalDismissed is called', async () => {
const newActions = {
...baseProps.actions,
updateOutgoingHook: jest.fn().mockReturnValue({data: 'data'}),
};
const props = {...baseProps, hook, actions: newActions};
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
wrapper.setState({showConfirmModal: true});
wrapper.instance().confirmModalDismissed();
expect(wrapper.state('showConfirmModal')).toEqual(false);
// Change callback URLs to trigger confirm modal
const callbackUrlsTextarea = container.querySelector('#callbackUrls') as HTMLTextAreaElement;
await userEvent.clear(callbackUrlsTextarea);
await userEvent.type(callbackUrlsTextarea, 'https://different.com/callback');
await userEvent.click(container.querySelector('#saveWebhook') as HTMLButtonElement);
await waitFor(() => {
expect(screen.getByText('Your changes may break the existing outgoing webhook. Are you sure you would like to update it?')).toBeInTheDocument();
});
// Click cancel to dismiss the confirm modal
await userEvent.click(screen.getByTestId('cancel-button'));
await waitFor(() => {
expect(screen.queryByText('Your changes may break the existing outgoing webhook. Are you sure you would like to update it?')).not.toBeInTheDocument();
});
});
test('should have match renderExtra', () => {
const props = {...baseProps, hook};
const wrapper = shallow<EditOutgoingWebhook>(
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper.instance().renderExtra()).toMatchSnapshot();
// renderExtra renders a ConfirmModal - verify its presence in the DOM
expect(container.querySelector('.integrations-backstage-modal')).toMatchSnapshot();
});
test('should have match when editOutgoingHook is called', () => {
const props = {...baseProps, hook};
const wrapper = shallow<EditOutgoingWebhook>(
test('should have match when editOutgoingHook is called', async () => {
const newActions = {
...baseProps.actions,
updateOutgoingHook: jest.fn().mockReturnValue({data: 'data'}),
};
const props = {...baseProps, hook, actions: newActions};
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
const instance = wrapper.instance();
instance.handleConfirmModal = jest.fn();
instance.submitHook = jest.fn();
wrapper.instance().editOutgoingHook(hook);
// Submit form without changing content_type, trigger_words or callback_urls
// should call submitHook directly (no confirm modal)
await userEvent.click(container.querySelector('#saveWebhook') as HTMLButtonElement);
expect(instance.handleConfirmModal).not.toHaveBeenCalled();
expect(instance.submitHook).toHaveBeenCalled();
// No confirm modal should appear since nothing breaking changed
expect(screen.queryByText('Your changes may break the existing outgoing webhook. Are you sure you would like to update it?')).not.toBeInTheDocument();
// submitHook should have been called which calls updateOutgoingHook
expect(newActions.updateOutgoingHook).toHaveBeenCalled();
});
test('should have match when submitHook is called on success', async () => {
@@ -125,17 +212,18 @@ describe('components/integrations/EditOutgoingWebhook', () => {
updateOutgoingHook: jest.fn().mockReturnValue({data: 'data'}),
};
const props = {...baseProps, hook, actions: newActions};
const wrapper = shallow<EditOutgoingWebhook>(
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
const instance = wrapper.instance();
wrapper.setState({showConfirmModal: true});
await instance.submitHook();
// Submit form - no breaking changes so submitHook is called directly
await userEvent.click(container.querySelector('#saveWebhook') as HTMLButtonElement);
expect(newActions.updateOutgoingHook).toHaveBeenCalledTimes(1);
expect(wrapper.state('serverError')).toEqual('');
expect(getHistory().push).toHaveBeenCalledWith(`/${team.name}/integrations/outgoing_webhooks`);
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith(`/${team.name}/integrations/outgoing_webhooks`);
});
});
test('should have match when submitHook is called on error', async () => {
@@ -144,14 +232,14 @@ describe('components/integrations/EditOutgoingWebhook', () => {
updateOutgoingHook: jest.fn().mockReturnValue({data: ''}),
};
const props = {...baseProps, hook, actions: newActions};
const wrapper = shallow<EditOutgoingWebhook>(
const {container} = renderWithContext(
<EditOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
const instance = wrapper.instance();
wrapper.setState({showConfirmModal: true});
await instance.submitHook();
// Submit form
await userEvent.click(container.querySelector('#saveWebhook') as HTMLButtonElement);
expect(wrapper.state('showConfirmModal')).toEqual(false);
expect(newActions.updateOutgoingHook).toHaveBeenCalledTimes(1);
});
});
@@ -1,13 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import React from 'react';
import InstalledCommand from 'components/integrations/installed_command';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/integrations/delete_integration_link', () => {
const ReactMock = require('react'); // eslint-disable-line @typescript-eslint/no-var-requires, global-require
return {
__esModule: true,
default: (props: {onDelete: () => void}) => ReactMock.createElement('button', {onClick: props.onDelete}, 'Delete'),
};
});
describe('components/integrations/InstalledCommand', () => {
const team = TestHelper.getTeamMock({name: 'team_name'});
const command = TestHelper.getCommandMock({
@@ -22,6 +31,17 @@ describe('components/integrations/InstalledCommand', () => {
});
const creator = TestHelper.getUserMock({username: 'username'});
const initialState = {
entities: {
general: {
config: {},
},
users: {
currentUserId: 'currentUserId',
},
},
};
const requiredProps = {
team,
command,
@@ -32,13 +52,16 @@ describe('components/integrations/InstalledCommand', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<InstalledCommand {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<InstalledCommand {...requiredProps}/>,
initialState,
);
expect(container).toMatchSnapshot();
const trigger = `- /${command.trigger} ${command.auto_complete_hint}`;
expect(wrapper.find('.item-details__trigger').text()).toBe(trigger);
expect(wrapper.find('.item-details__name').text()).toBe(command.display_name);
expect(wrapper.find('.item-details__description').text()).toBe(command.description);
expect(container.querySelector('.item-details__trigger')?.textContent).toBe(trigger);
expect(container.querySelector('.item-details__name')?.textContent).toBe(command.display_name);
expect(container.querySelector('.item-details__description')?.textContent).toBe(command.description);
});
test('should match snapshot, not autocomplete, no display_name/description/auto_complete_hint', () => {
@@ -51,35 +74,45 @@ describe('components/integrations/InstalledCommand', () => {
});
const props = {...requiredProps, command: minCommand};
const wrapper = shallow(<InstalledCommand {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<InstalledCommand {...props}/>,
initialState,
);
expect(container).toMatchSnapshot();
const trigger = `- /${command.trigger}`;
expect(wrapper.find('.item-details__trigger').text()).toBe(trigger);
expect(container.querySelector('.item-details__trigger')?.textContent).toBe(trigger);
});
test('should call onRegenToken function', () => {
test('should call onRegenToken function', async () => {
const onRegenToken = jest.fn();
const canChange = true;
const props = {...requiredProps, onRegenToken, canChange};
const wrapper = shallow(<InstalledCommand {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<InstalledCommand {...props}/>,
initialState,
);
expect(container).toMatchSnapshot();
wrapper.find('div.item-actions button').first().simulate('click', {preventDefault: jest.fn()});
await userEvent.click(screen.getByRole('button', {name: 'Regenerate Token'}));
expect(onRegenToken).toHaveBeenCalledTimes(1);
expect(onRegenToken).toHaveBeenCalledWith(props.command);
});
test('should call onDelete function', () => {
test('should call onDelete function', async () => {
const onDelete = jest.fn();
const canChange = true;
const props = {...requiredProps, onDelete, canChange};
const wrapper = shallow<InstalledCommand>(<InstalledCommand {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<InstalledCommand {...props}/>,
initialState,
);
expect(container).toMatchSnapshot();
await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
wrapper.instance().handleDelete();
expect(onDelete).toHaveBeenCalledTimes(1);
expect(onDelete).toHaveBeenCalledWith(props.command);
});
@@ -88,7 +121,10 @@ describe('components/integrations/InstalledCommand', () => {
const filter = 'no_match';
const props = {...requiredProps, filter};
const wrapper = shallow(<InstalledCommand {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(
<InstalledCommand {...props}/>,
initialState,
);
expect(container.firstChild).toBeNull();
});
});
@@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import React from 'react';
import {Link} from 'react-router-dom';
import type {IncomingWebhook} from '@mattermost/types/integrations';
import DeleteIntegrationLink from 'components/integrations/delete_integration_link';
import InstalledIncomingWebhook from 'components/integrations/installed_incoming_webhook';
import {renderWithContext} from 'tests/react_testing_utils';
describe('components/integrations/InstalledIncomingWebhook', () => {
const incomingWebhook: IncomingWebhook = {
id: '9w96t4nhbfdiij64wfqors4i1r',
@@ -28,349 +28,139 @@ describe('components/integrations/InstalledIncomingWebhook', () => {
const teamId = 'testteamid';
test('should match snapshot', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const initialState = {
entities: {
general: {
config: {},
},
users: {
currentUserId: 'currentUserId',
},
},
};
const wrapper = shallow<InstalledIncomingWebhook>(
const baseProps = {
incomingWebhook,
onDelete: jest.fn(),
creator: {username: 'creator'},
team: {
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O' as const,
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
},
channel: {
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O' as const,
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
},
};
test('should match snapshot', () => {
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
incomingWebhook={incomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
{...baseProps}
canChange={true}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not have edit and delete actions if user does not have permissions to change', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow<InstalledIncomingWebhook>(
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
incomingWebhook={incomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
{...baseProps}
canChange={false}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper.find('.item-actions').length).toBe(0);
expect(container.querySelector('.item-actions')).toBeNull();
});
test('should have edit and delete actions if user can change webhook', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow<InstalledIncomingWebhook>(
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
incomingWebhook={incomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
{...baseProps}
canChange={true}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper.find('.item-actions').find(Link).exists()).toBe(true);
expect(wrapper.find('.item-actions').find(DeleteIntegrationLink).exists()).toBe(true);
expect(container.querySelector('.item-actions')).not.toBeNull();
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Delete')).toBeInTheDocument();
});
test('Should have the same name and description on view as it has in incomingWebhook', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow<InstalledIncomingWebhook>(
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
incomingWebhook={incomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
{...baseProps}
canChange={false}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper.find('.item-details__description').text()).toBe('build status');
expect(wrapper.find('.item-details__name').text()).toBe('build');
expect(container.querySelector('.item-details__description')!.textContent).toBe('build status');
expect(container.querySelector('.item-details__name')!.textContent).toBe('build');
});
test('Should not display description as it is null', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const newIncomingWebhook: IncomingWebhook = {...incomingWebhook, description: ''};
const wrapper = shallow<InstalledIncomingWebhook>(
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
{...baseProps}
incomingWebhook={newIncomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
canChange={false}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper.find('.item-details__description').length).toBe(0);
expect(container.querySelector('.item-details__description')).toBeNull();
});
test('Should not render any nodes as there are no filtered results', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow<InstalledIncomingWebhook>(
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
incomingWebhook={incomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
{...baseProps}
filter={'someLongText'}
canChange={false}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper.getElement()).toBe(null);
expect(container.firstChild).toBeNull();
});
test('Should render a webhook item as filtered result is true', () => {
function emptyFunction() {} //eslint-disable-line no-empty-function
const wrapper = shallow<InstalledIncomingWebhook>(
const {container} = renderWithContext(
<InstalledIncomingWebhook
key={1}
incomingWebhook={incomingWebhook}
onDelete={emptyFunction}
creator={{username: 'creator'}}
{...baseProps}
filter={'buil'}
canChange={true}
team={{
id: teamId,
name: 'test',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
type: 'O',
display_name: 'name',
scheme_id: 'id',
allow_open_invite: false,
group_constrained: false,
description: '',
email: '',
company_name: '',
allowed_domains: '',
invite_id: '',
}}
channel={{
id: '1jiw9kphbjrntfyrm7xpdcya4o',
name: 'town-square',
create_at: 1502455422406,
delete_at: 0,
update_at: 1502455422406,
team_id: teamId,
type: 'O',
display_name: 'name',
header: 'header',
purpose: 'purpose',
last_post_at: 0,
last_root_post_at: 0,
creator_id: 'id',
scheme_id: 'id',
group_constrained: false,
}}
/>,
initialState,
);
expect(wrapper.find('.item-details').exists()).toBe(true);
expect(container.querySelector('.item-details')).not.toBeNull();
});
});
@@ -1,585 +1,425 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/InstalledOAuthApp should filter out OAuthApp 1`] = `""`;
exports[`components/integrations/InstalledOAuthApp should filter out OAuthApp 1`] = `<div />`;
exports[`components/integrations/InstalledOAuthApp should match snapshot 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
class="item-details"
>
<strong
className="item-details__name"
>
testApp
</strong>
<div
className="item-actions"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<button
className="style--none color--link"
id="showSecretButton"
onClick={[Function]}
<strong
class="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Show Secret"
id="installed_integrations.showSecret"
/>
</button>
-
<button
className="style--none color--link"
id="regenerateSecretButton"
onClick={[Function]}
testApp
</strong>
<div
class="item-actions"
>
<MemoizedFormattedMessage
defaultMessage="Regenerate Secret"
id="installed_integrations.regenSecret"
/>
</button>
-
<Link
key="edit"
to="/team_name/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
key="delete"
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the OAuth 2.0 application and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_oauth_apps.delete.confirm"
/>
}
onDelete={[Function]}
/>
<button
class="style--none color--link"
id="showSecretButton"
>
Show Secret
</button>
-
<button
class="style--none color--link"
id="regenerateSecretButton"
>
Regenerate Secret
</button>
-
<a
href="/team_name/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
testing
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__description"
>
testing
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Is Trusted: "
id="installed_oauth_apps.is_trusted"
/>
<strong>
<MemoizedFormattedMessage
defaultMessage="Yes"
id="installed_oauth_apps.trusted.yes"
<span
class="item-details__url word-break--all"
>
Is Trusted:
<strong>
Yes
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client ID:
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client ID: "
id="installed_integrations.client_id"
/>
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Client Id",
"id": "integrations.copy_client_id",
}
}
value="facxd9wpzpbpfp8pad78xj75pr"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Client Secret:
<strong>
***************
</strong>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client Secret: "
id="installed_integrations.client_secret"
/>
<strong>
***************
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__url word-break--all"
>
Callback URLs: https://test.com/callback, https://test.com/callback2
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Callback URLs: {urls}"
id="installed_integrations.callback_urls"
values={
Object {
"urls": "https://test.com/callback, https://test.com/callback2",
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1501365458934,
"creator": "somename",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by somename on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledOAuthApp should match snapshot from app 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="integration__icon integration-list__icon"
>
<strong
className="item-details__name"
>
testApp
</strong>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
className="item-details__row"
class="item-details"
>
<span
className="item-details__description"
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
testing
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
<strong
class="item-details__name"
>
testApp
</strong>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Managed by Apps Framework"
id="installed_integrations.fromApp"
/>
</span>
<span
class="item-details__description"
>
testing
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Managed by Apps Framework
</span>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledOAuthApp should match snapshot, on error 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
class="item-details"
>
<strong
className="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Unnamed OAuth 2.0 Application"
id="installed_integrations.unnamed_oauth_app"
/>
</strong>
<div
className="item-actions"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<button
className="style--none color--link"
id="showSecretButton"
onClick={[Function]}
<strong
class="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Show Secret"
id="installed_integrations.showSecret"
/>
</button>
-
<button
className="style--none color--link"
id="regenerateSecretButton"
onClick={[Function]}
testApp
</strong>
<div
class="item-actions"
>
<MemoizedFormattedMessage
defaultMessage="Regenerate Secret"
id="installed_integrations.regenSecret"
/>
</button>
-
<Link
key="edit"
to="/team_name/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
key="delete"
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the OAuth 2.0 application and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_oauth_apps.delete.confirm"
/>
}
onDelete={[Function]}
/>
<button
class="style--none color--link"
id="showSecretButton"
>
Show Secret
</button>
-
<button
class="style--none color--link"
id="regenerateSecretButton"
>
Regenerate Secret
</button>
-
<a
href="/team_name/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
</div>
<Memo(FormError)
error="error"
/>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
testing
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__description"
>
testing
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Is Trusted: "
id="installed_oauth_apps.is_trusted"
/>
<strong>
<MemoizedFormattedMessage
defaultMessage="No"
id="installed_oauth_apps.trusted.no"
<span
class="item-details__url word-break--all"
>
Is Trusted:
<strong>
Yes
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client ID:
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client ID: "
id="installed_integrations.client_id"
/>
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Client Id",
"id": "integrations.copy_client_id",
}
}
value="facxd9wpzpbpfp8pad78xj75pr"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Client Secret:
<strong>
***************
</strong>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client Secret: "
id="installed_integrations.client_secret"
/>
<strong>
***************
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__url word-break--all"
>
Callback URLs: https://test.com/callback, https://test.com/callback2
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Callback URLs: {urls}"
id="installed_integrations.callback_urls"
values={
Object {
"urls": "https://test.com/callback, https://test.com/callback2",
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1501365458934,
"creator": "somename",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by somename on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledOAuthApp should match snapshot, when oauthApp is without name and not trusted 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
class="item-details"
>
<strong
className="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Unnamed OAuth 2.0 Application"
id="installed_integrations.unnamed_oauth_app"
/>
</strong>
<div
className="item-actions"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<button
className="style--none color--link"
id="showSecretButton"
onClick={[Function]}
<strong
class="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Show Secret"
id="installed_integrations.showSecret"
/>
</button>
-
<button
className="style--none color--link"
id="regenerateSecretButton"
onClick={[Function]}
Unnamed OAuth 2.0 Application
</strong>
<div
class="item-actions"
>
<MemoizedFormattedMessage
defaultMessage="Regenerate Secret"
id="installed_integrations.regenSecret"
/>
</button>
-
<Link
key="edit"
to="/team_name/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
key="delete"
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="This action permanently deletes the OAuth 2.0 application and breaks any integrations using it. Are you sure you want to delete it?"
id="installed_oauth_apps.delete.confirm"
/>
}
onDelete={[Function]}
/>
<button
class="style--none color--link"
id="showSecretButton"
>
Show Secret
</button>
-
<button
class="style--none color--link"
id="regenerateSecretButton"
>
Regenerate Secret
</button>
-
<a
href="/team_name/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__description"
<div
class="item-details__row"
>
testing
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__description"
>
testing
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Is Trusted: "
id="installed_oauth_apps.is_trusted"
/>
<strong>
<MemoizedFormattedMessage
defaultMessage="No"
id="installed_oauth_apps.trusted.no"
<span
class="item-details__url word-break--all"
>
Is Trusted:
<strong>
No
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client ID:
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client ID: "
id="installed_integrations.client_id"
/>
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<Memo(CopyText)
label={
Object {
"defaultMessage": "Copy Client Id",
"id": "integrations.copy_client_id",
}
}
value="facxd9wpzpbpfp8pad78xj75pr"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<span
class="item-details__token"
>
Client Secret:
<strong>
***************
</strong>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client Secret: "
id="installed_integrations.client_secret"
/>
<strong>
***************
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__url word-break--all"
>
Callback URLs: https://test.com/callback, https://test.com/callback2
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Callback URLs: {urls}"
id="installed_integrations.callback_urls"
values={
Object {
"urls": "https://test.com/callback, https://test.com/callback2",
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1501365458934,
"creator": "somename",
}
}
/>
</span>
<span
class="item-details__creation"
>
Created by somename on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
</div>
@@ -1,14 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import DeleteIntegrationLink from 'components/integrations/delete_integration_link';
import InstalledOAuthApp from 'components/integrations/installed_oauth_app/installed_oauth_app';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/integrations/delete_integration_link', () => {
return {
__esModule: true,
default: (props: {onDelete: () => void}) => (
<button onClick={props.onDelete}>{'Delete'}</button>
),
};
});
describe('components/integrations/InstalledOAuthApp', () => {
const FAKE_SECRET = '***************';
const team = TestHelper.getTeamMock({name: 'team_name'});
@@ -43,109 +51,130 @@ describe('components/integrations/InstalledOAuthApp', () => {
test('should match snapshot', () => {
const props = {...baseProps, team};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot from app', () => {
const props = {...baseProps, team, fromApp: true};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, when oauthApp is without name and not trusted', () => {
const props = {...baseProps, team};
props.oauthApp.name = '';
props.oauthApp.is_trusted = false;
const wrapper = shallow(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, on error', () => {
const props = {...baseProps, team};
const wrapper = shallow(
<InstalledOAuthApp {...props}/>,
);
wrapper.setState({error: 'error'});
expect(wrapper).toMatchSnapshot();
});
test('should call onRegenerateSecret function', () => {
const props = {
...baseProps,
team,
oauthApp: {
...oauthApp,
name: '',
is_trusted: false,
},
};
baseProps.onRegenerateSecret.mockResolvedValue({});
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOAuthApp {...props}/>,
);
wrapper.find('#regenerateSecretButton').simulate('click', {preventDefault: jest.fn()});
expect(container).toMatchSnapshot();
});
expect(baseProps.onRegenerateSecret).toHaveBeenCalled();
expect(baseProps.onRegenerateSecret).toHaveBeenCalledWith(oauthApp.id);
test('should match snapshot, on error', () => {
const onRegenerateSecret = jest.fn().mockResolvedValue({error: {message: 'error'}});
const props = {...baseProps, team, onRegenerateSecret};
const {container} = renderWithContext(
<InstalledOAuthApp {...props}/>,
);
// Trigger error by clicking regenerate
screen.getByText('Regenerate Secret').click();
waitFor(() => {
expect(container).toMatchSnapshot();
});
});
test('should call onRegenerateSecret function', async () => {
const onRegenerateSecret = jest.fn().mockResolvedValue({});
const props = {
...baseProps,
team,
onRegenerateSecret,
};
renderWithContext(
<InstalledOAuthApp {...props}/>,
);
await userEvent.click(screen.getByText('Regenerate Secret'));
expect(onRegenerateSecret).toHaveBeenCalled();
expect(onRegenerateSecret).toHaveBeenCalledWith(oauthApp.id);
});
test('should filter out OAuthApp', () => {
const filter = 'filter';
const props = {...baseProps, filter};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match state on button clicks, both showSecretButton and hideSecretButton', () => {
test('should match state on button clicks, both showSecretButton and hideSecretButton', async () => {
const props = {...baseProps, team};
const wrapper = shallow(
renderWithContext(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper.find('#showSecretButton').exists()).toBe(true);
expect(wrapper.find('#hideSecretButton').exists()).toBe(false);
wrapper.find('#showSecretButton').simulate('click', {preventDefault: jest.fn()});
expect(wrapper.state('clientSecret')).toEqual(oauthApp.client_secret);
expect(wrapper.find('#showSecretButton').exists()).toBe(false);
expect(wrapper.find('#hideSecretButton').exists()).toBe(true);
// Initially show secret button is visible, hide secret button is not
expect(screen.getByText('Show Secret')).toBeInTheDocument();
expect(screen.queryByText('Hide Secret')).not.toBeInTheDocument();
expect(screen.getByText(FAKE_SECRET)).toBeInTheDocument();
wrapper.find('#hideSecretButton').simulate('click', {preventDefault: jest.fn()});
expect(wrapper.state('clientSecret')).toEqual(FAKE_SECRET);
expect(wrapper.find('#showSecretButton').exists()).toBe(true);
expect(wrapper.find('#hideSecretButton').exists()).toBe(false);
// Click show secret
await userEvent.click(screen.getByText('Show Secret'));
expect(screen.getByText(oauthApp.client_secret)).toBeInTheDocument();
expect(screen.queryByText('Show Secret')).not.toBeInTheDocument();
expect(screen.getByText('Hide Secret')).toBeInTheDocument();
// Click hide secret
await userEvent.click(screen.getByText('Hide Secret'));
expect(screen.getByText(FAKE_SECRET)).toBeInTheDocument();
expect(screen.getByText('Show Secret')).toBeInTheDocument();
expect(screen.queryByText('Hide Secret')).not.toBeInTheDocument();
});
test('should match on handleRegenerate', () => {
test('should match on handleRegenerate', async () => {
const onRegenerateSecret = jest.fn().mockResolvedValue({});
const props = {
...baseProps,
team,
onRegenerateSecret,
};
baseProps.onRegenerateSecret.mockResolvedValue({});
const wrapper = shallow(
renderWithContext(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper.find('#regenerateSecretButton').exists()).toBe(true);
wrapper.find('#regenerateSecretButton').simulate('click', {preventDefault: jest.fn()});
expect(baseProps.onRegenerateSecret).toHaveBeenCalled();
expect(baseProps.onRegenerateSecret).toHaveBeenCalledWith(oauthApp.id);
expect(screen.getByText('Regenerate Secret')).toBeInTheDocument();
await userEvent.click(screen.getByText('Regenerate Secret'));
expect(onRegenerateSecret).toHaveBeenCalled();
expect(onRegenerateSecret).toHaveBeenCalledWith(oauthApp.id);
});
test('should have called props.onDelete on handleDelete ', () => {
test('should have called props.onDelete on handleDelete ', async () => {
const newOnDelete = jest.fn();
const props = {...baseProps, team, onDelete: newOnDelete};
const wrapper = shallow(
renderWithContext(
<InstalledOAuthApp {...props}/>,
);
expect(wrapper.find(DeleteIntegrationLink).exists()).toBe(true);
wrapper.find(DeleteIntegrationLink).props().onDelete();
expect(screen.getByText('Delete')).toBeInTheDocument();
await userEvent.click(screen.getByText('Delete'));
expect(newOnDelete).toHaveBeenCalled();
expect(newOnDelete).toHaveBeenCalledWith(oauthApp);
});
@@ -1,163 +1,614 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/InstalledOAuthApps should match snapshot 1`] = `
<BackstageList
addButtonId="addOauthApp"
addLink="/test/integrations/oauth2-apps/add"
addText="Add OAuth 2.0 Application"
emptyText={
<Memo(MemoizedFormattedMessage)
defaultMessage="No OAuth 2.0 Applications found"
id="installed_oauth_apps.empty"
/>
}
emptyTextSearch={
<Memo(MemoizedFormattedMessage)
defaultMessage="No OAuth 2.0 Applications match <strong>{searchTerm}</strong>"
id="installed_oauth_apps.emptySearch"
values={
Object {
"strong": [Function],
}
}
/>
}
header={
<Memo(MemoizedFormattedMessage)
defaultMessage="OAuth 2.0 Applications"
id="installed_oauth2_apps.header"
/>
}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Create {oauthApplications} to securely integrate bots and third-party apps with Mattermost. Visit the {appDirectory} to find available self-hosted apps."
id="installed_oauth_apps.help"
values={
Object {
"appDirectory": <ForwardRef
href="https://mattermost.com/marketplace/"
location="installed_oauth_apps"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="App Directory"
id="installed_oauth_apps.help.appDirectory"
/>
</ForwardRef>,
"oauthApplications": <ForwardRef
href="https://mattermost.com/pl/setup-oauth-2.0"
location="installed_oauth_apps"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="OAuth 2.0 applications"
id="installed_oauth_apps.help.oauthApplications"
/>
</ForwardRef>,
}
}
/>
}
loading={true}
searchPlaceholder="Search OAuth 2.0 Applications"
>
<Component />
</BackstageList>
`;
exports[`components/integrations/InstalledOAuthApps should match snapshot 2`] = `
<div>
<Connect(InstalledOAuthApp)
creatorName=""
fromApp={false}
key="facxd9wpzpbpfp8pad78xj75pr"
oauthApp={
Object {
"callback_urls": Array [
"https://test.com/callback",
],
"client_secret": "88cxd9wpzpbpfp8pad78xj75pr",
"create_at": 1501365458934,
"creator_id": "88oybd1dwfdoxpkpw1h5kpbyco",
"description": "testing",
"homepage": "https://test.com",
"icon_url": "https://test.com/icon",
"id": "facxd9wpzpbpfp8pad78xj75pr",
"is_trusted": false,
"name": "firstApp",
"update_at": 1501365458934,
}
}
onDelete={[Function]}
onRegenerateSecret={[MockFunction]}
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
OAuth 2.0 Applications
</h1>
<a
class="add-link"
href="/test/integrations/oauth2-apps/add"
>
<button
class="btn btn-primary"
id="addOauthApp"
type="button"
>
<span>
Add OAuth 2.0 Application
</span>
</button>
</a>
</div>
<div
class="backstage-filters"
>
<div
class="backstage-filter__search"
>
<i
class="fa fa-search"
title="Search Icon"
/>
<input
class="form-control"
id="searchInput"
placeholder="Search OAuth 2.0 Applications"
type="search"
value=""
/>
</div>
</div>
<span
class="backstage-list__help"
>
Create
<a
href="https://mattermost.com/pl/setup-oauth-2.0?utm_source=mattermost&utm_medium=in-product&utm_content=installed_oauth_apps&uid=currentUserId&sid=&edition=team&server_version="
location="installed_oauth_apps"
rel="noopener noreferrer"
target="_blank"
>
OAuth 2.0 applications
</a>
to securely integrate bots and third-party apps with Mattermost. Visit the
<a
href="https://mattermost.com/marketplace/?utm_source=mattermost&utm_medium=in-product&utm_content=installed_oauth_apps&uid=currentUserId&sid=&edition=team&server_version="
location="installed_oauth_apps"
rel="noopener noreferrer"
target="_blank"
>
App Directory
</a>
to find available self-hosted apps.
</span>
<div
class="backstage-list"
>
<div
class="backstage-list__item"
>
<div
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
class="item-details"
>
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong
class="item-details__name"
>
firstApp
</strong>
<div
class="item-actions"
>
<button
class="style--none color--link"
id="showSecretButton"
>
Show Secret
</button>
-
<button
class="style--none color--link"
id="regenerateSecretButton"
>
Regenerate Secret
</button>
-
<a
href="/test/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<div
class="item-details__row"
>
<span
class="item-details__description"
>
testing
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Is Trusted:
<strong>
No
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client ID:
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client Secret:
<strong>
***************
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Callback URLs: https://test.com/callback
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
<div
class="backstage-list__item"
>
<div
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test2.com/icon"
/>
</div>
<div
class="item-details"
>
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong
class="item-details__name"
>
secondApp
</strong>
<div
class="item-actions"
>
<button
class="style--none color--link"
id="showSecretButton"
>
Show Secret
</button>
-
<button
class="style--none color--link"
id="regenerateSecretButton"
>
Regenerate Secret
</button>
-
<a
href="/test/integrations/oauth2-apps/edit?id=fzcxd9wpzpbpfp8pad78xj75pr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<div
class="item-details__row"
>
<span
class="item-details__description"
>
testing2
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Is Trusted:
<strong>
Yes
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client ID:
<strong>
fzcxd9wpzpbpfp8pad78xj75pr
</strong>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client Secret:
<strong>
***************
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Callback URLs: https://test2.com/callback, https://test2.com/callback2
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
</div>
<div
class="backstage-footer"
>
<div
class="backstage-footer__cell"
>
1 - 2 of 2
<button
aria-label="Previous"
class="btn btn-quaternary btn-icon btn-sm ml-2 prev disabled"
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
aria-label="Next"
class="btn btn-quaternary btn-icon btn-sm next disabled"
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledOAuthApps should match snapshot 3`] = `<div />`;
exports[`components/integrations/InstalledOAuthApps should match snapshot for Apps 1`] = `
<div>
<Connect(InstalledOAuthApp)
creatorName=""
fromApp={true}
key="fzcxd9wpzpbpfp8pad78xj75pr"
oauthApp={
Object {
"callback_urls": Array [
"https://test2.com/callback",
"https://test2.com/callback2",
],
"client_secret": "decxd9wpzpbpfp8pad78xj75pr",
"create_at": 1501365459984,
"creator_id": "88oybd2dwfdoxpkpw1h5kpbyco",
"description": "testing2",
"homepage": "https://test2.com",
"icon_url": "https://test2.com/icon",
"id": "fzcxd9wpzpbpfp8pad78xj75pr",
"is_trusted": true,
"name": "secondApp",
"update_at": 1501365479988,
}
}
onDelete={[Function]}
onRegenerateSecret={[MockFunction]}
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "team_id",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
OAuth 2.0 Applications
</h1>
<a
class="add-link"
href="/test/integrations/oauth2-apps/add"
>
<button
class="btn btn-primary"
id="addOauthApp"
type="button"
>
<span>
Add OAuth 2.0 Application
</span>
</button>
</a>
</div>
<div
class="backstage-filters"
>
<div
class="backstage-filter__search"
>
<i
class="fa fa-search"
title="Search Icon"
/>
<input
class="form-control"
id="searchInput"
placeholder="Search OAuth 2.0 Applications"
type="search"
value=""
/>
</div>
</div>
<span
class="backstage-list__help"
>
Create
<a
href="https://mattermost.com/pl/setup-oauth-2.0?utm_source=mattermost&utm_medium=in-product&utm_content=installed_oauth_apps&uid=currentUserId&sid=&edition=team&server_version="
location="installed_oauth_apps"
rel="noopener noreferrer"
target="_blank"
>
OAuth 2.0 applications
</a>
to securely integrate bots and third-party apps with Mattermost. Visit the
<a
href="https://mattermost.com/marketplace/?utm_source=mattermost&utm_medium=in-product&utm_content=installed_oauth_apps&uid=currentUserId&sid=&edition=team&server_version="
location="installed_oauth_apps"
rel="noopener noreferrer"
target="_blank"
>
App Directory
</a>
to find available self-hosted apps.
</span>
<div
class="backstage-list"
>
<div
class="backstage-list__item"
>
<div
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test.com/icon"
/>
</div>
<div
class="item-details"
>
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong
class="item-details__name"
>
firstApp
</strong>
<div
class="item-actions"
>
<button
class="style--none color--link"
id="showSecretButton"
>
Show Secret
</button>
-
<button
class="style--none color--link"
id="regenerateSecretButton"
>
Regenerate Secret
</button>
-
<a
href="/test/integrations/oauth2-apps/edit?id=facxd9wpzpbpfp8pad78xj75pr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<div
class="item-details__row"
>
<span
class="item-details__description"
>
testing
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Is Trusted:
<strong>
No
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client ID:
<strong>
facxd9wpzpbpfp8pad78xj75pr
</strong>
<button
aria-label="Copy Client Id"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Client Secret:
<strong>
***************
</strong>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Callback URLs: https://test.com/callback
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
<div
class="backstage-list__item"
>
<div
class="integration__icon integration-list__icon"
>
<img
alt="get app screenshot"
src="https://test2.com/icon"
/>
</div>
<div
class="item-details"
>
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong
class="item-details__name"
>
secondApp
</strong>
</div>
<div
class="item-details__row"
>
<span
class="item-details__description"
>
testing2
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Managed by Apps Framework
</span>
</div>
</div>
</div>
</div>
<div
class="backstage-footer"
>
<div
class="backstage-footer__cell"
>
1 - 2 of 2
<button
aria-label="Previous"
class="btn btn-quaternary btn-icon btn-sm ml-2 prev disabled"
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
aria-label="Next"
class="btn btn-quaternary btn-icon btn-sm next disabled"
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
</div>
</div>
</div>
`;
@@ -1,14 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import React from 'react';
import BackstageList from 'components/backstage/components/backstage_list';
import InstalledOAuthApps from 'components/integrations/installed_oauth_apps/installed_oauth_apps';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/integrations/delete_integration_link', () => {
const React = require('react'); // eslint-disable-line @typescript-eslint/no-var-requires, global-require
return {
__esModule: true,
default: (props: {onDelete: () => void}) => React.createElement('button', {onClick: props.onDelete}, 'Delete'),
};
});
describe('components/integrations/InstalledOAuthApps', () => {
const oauthApps = {
facxd9wpzpbpfp8pad78xj75pr: {
@@ -39,6 +47,18 @@ describe('components/integrations/InstalledOAuthApps', () => {
},
};
const initialState = {
entities: {
general: {
config: {},
},
users: {
currentUserId: 'currentUserId',
profiles: {},
},
},
};
const baseProps = {
team: TestHelper.getTeamMock({
name: 'test',
@@ -46,68 +66,105 @@ describe('components/integrations/InstalledOAuthApps', () => {
oauthApps,
canManageOauth: true,
actions: {
loadOAuthAppsAndProfiles: jest.fn(),
loadOAuthAppsAndProfiles: jest.fn().mockResolvedValue({}),
regenOAuthAppSecret: jest.fn(),
deleteOAuthApp: jest.fn(),
},
enableOAuthServiceProvider: true,
appsOAuthAppIDs: [],
appsOAuthAppIDs: [] as string[],
};
test('should match snapshot', () => {
const newGetOAuthApps = jest.fn().mockImplementation(
() => {
return new Promise((resolve) => {
process.nextTick(() => resolve({}));
});
},
test('should match snapshot', async () => {
const {container} = renderWithContext(
<InstalledOAuthApps {...baseProps}/>,
initialState,
);
const props = {...baseProps};
props.actions.loadOAuthAppsAndProfiles = newGetOAuthApps;
const wrapper = shallow<InstalledOAuthApps>(<InstalledOAuthApps {...props}/>);
// Wait for loading to complete
await screen.findByText('firstApp');
expect(wrapper).toMatchSnapshot();
expect(shallow(<div>{wrapper.instance().oauthApps('first')}</div>)).toMatchSnapshot(); // successful filter
expect(shallow(<div>{wrapper.instance().oauthApps('ZZZ')}</div>)).toMatchSnapshot(); // unsuccessful filter
expect(shallow(<div>{wrapper.instance().oauthApps()}</div>).find('Connect(InstalledOAuthApp)').length).toBe(2); // no filter, should return all
expect(wrapper.find(BackstageList).props().addLink).toEqual('/test/integrations/oauth2-apps/add');
expect(wrapper.find(BackstageList).props().addText).toEqual('Add OAuth 2.0 Application');
expect(container).toMatchSnapshot();
wrapper.setProps({canManageOauth: false});
expect(wrapper.find(BackstageList).props().addLink).toBeFalsy();
expect(wrapper.find(BackstageList).props().addText).toBeFalsy();
// Both apps should be rendered
expect(screen.getByText('firstApp')).toBeInTheDocument();
expect(screen.getByText('secondApp')).toBeInTheDocument();
// Should have add link and add text when canManageOauth is true
expect(screen.getByText('Add OAuth 2.0 Application')).toBeInTheDocument();
// Filter by typing in search input
const searchInput = screen.getByPlaceholderText('Search OAuth 2.0 Applications');
await userEvent.type(searchInput, 'first');
expect(screen.getByText('firstApp')).toBeInTheDocument();
expect(screen.queryByText('secondApp')).not.toBeInTheDocument();
// Clear and type non-matching filter
await userEvent.clear(searchInput);
await userEvent.type(searchInput, 'ZZZ');
expect(screen.queryByText('firstApp')).not.toBeInTheDocument();
expect(screen.queryByText('secondApp')).not.toBeInTheDocument();
// Clear filter to show all apps again
await userEvent.clear(searchInput);
expect(screen.getByText('firstApp')).toBeInTheDocument();
expect(screen.getByText('secondApp')).toBeInTheDocument();
// Re-render with canManageOauth=false, add link should not be present
const {container: container2} = renderWithContext(
<InstalledOAuthApps
{...baseProps}
canManageOauth={false}
/>,
initialState,
);
await screen.findAllByText('firstApp');
expect(container2.querySelector('#addOauthApp')).not.toBeInTheDocument();
});
test('should match snapshot for Apps', () => {
const newGetOAuthApps = jest.fn().mockImplementation(
() => {
return new Promise((resolve) => {
process.nextTick(() => resolve({}));
});
},
);
test('should match snapshot for Apps', async () => {
const props = {
...baseProps,
appsOAuthAppIDs: ['fzcxd9wpzpbpfp8pad78xj75pr'],
};
props.actions.loadOAuthAppsAndProfiles = newGetOAuthApps;
const wrapper = shallow<InstalledOAuthApps>(<InstalledOAuthApps {...props}/>);
expect(shallow(<div>{wrapper.instance().oauthApps('second')}</div>)).toMatchSnapshot(); // successful filter
});
test('should props.deleteOAuthApp on deleteOAuthApp', () => {
const newDeleteOAuthApp = jest.fn();
const props = {...baseProps};
props.actions.deleteOAuthApp = newDeleteOAuthApp;
const wrapper = shallow<InstalledOAuthApps>(
const {container} = renderWithContext(
<InstalledOAuthApps {...props}/>,
initialState,
);
wrapper.instance().deleteOAuthApp(oauthApps.facxd9wpzpbpfp8pad78xj75pr);
expect(newDeleteOAuthApp).toHaveBeenCalled();
expect(newDeleteOAuthApp).toHaveBeenCalledWith(oauthApps.facxd9wpzpbpfp8pad78xj75pr.id);
// Wait for loading to complete
await screen.findByText('firstApp');
expect(container).toMatchSnapshot();
// The app managed by Apps Framework should show managed text
expect(screen.getByText('Managed by Apps Framework')).toBeInTheDocument();
});
test('should props.deleteOAuthApp on deleteOAuthApp', async () => {
const deleteOAuthApp = jest.fn();
const props = {
...baseProps,
actions: {
...baseProps.actions,
deleteOAuthApp,
},
};
renderWithContext(
<InstalledOAuthApps {...props}/>,
initialState,
);
// Wait for loading to complete
await screen.findByText('firstApp');
// Click the first Delete button (mocked DeleteIntegrationLink)
const deleteButtons = screen.getAllByRole('button', {name: 'Delete'});
await userEvent.click(deleteButtons[0]);
expect(deleteOAuthApp).toHaveBeenCalled();
expect(deleteOAuthApp).toHaveBeenCalledWith(oauthApps.facxd9wpzpbpfp8pad78xj75pr.id);
});
});
@@ -1,20 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {Link} from 'react-router-dom';
import type {Channel} from '@mattermost/types/channels';
import type {OutgoingWebhook} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import type {DeepPartial} from '@mattermost/types/utilities';
import DeleteIntegrationLink from 'components/integrations/delete_integration_link';
import InstalledOutgoingWebhook, {matchesFilter} from 'components/integrations/installed_outgoing_webhook';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
jest.mock('components/integrations/delete_integration_link', () => {
return (props: {onDelete: () => void}) => (
<button onClick={props.onDelete}>{'Delete'}</button>
);
});
const initialState: DeepPartial<GlobalState> = {
entities: {
general: {
config: {},
},
},
};
describe('components/integrations/InstalledOutgoingWebhook', () => {
const team: Team = TestHelper.getTeamMock({
id: 'testteamid',
@@ -60,113 +75,137 @@ describe('components/integrations/InstalledOutgoingWebhook', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOutgoingWebhook {...baseProps}/>,
initialState as GlobalState,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should not have edit and delete actions if user does not have permissions to change', () => {
const newCanChange = false;
const props = {...baseProps, canChange: newCanChange};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper.find('.item-actions').length).toBe(0);
expect(container.querySelector('.item-actions')).not.toBeInTheDocument();
});
test('should have edit and delete actions if user can change webhook', () => {
const wrapper = shallow(
renderWithContext(
<InstalledOutgoingWebhook {...baseProps}/>,
initialState as GlobalState,
);
expect(wrapper.find('.item-actions').find(Link).exists()).toBe(true);
expect(wrapper.find('.item-actions').find(DeleteIntegrationLink).exists()).toBe(true);
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Delete')).toBeInTheDocument();
});
test('Should have the same name and description on view as it has in outgoingWebhook', () => {
const newCanChange = false;
const props = {...baseProps, canChange: newCanChange};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper.find('.item-details__description').text()).toBe('build status');
expect(wrapper.find('.item-details__name').text()).toBe('build');
expect(container.querySelector('.item-details__description')).toHaveTextContent('build status');
expect(container.querySelector('.item-details__name')).toHaveTextContent('build');
});
test('Should not display description as it is null', () => {
const newOutgoingWebhook = TestHelper.getOutgoingWebhookMock({...outgoingWebhook, description: undefined});
const props = {...baseProps, outgoingWebhook: newOutgoingWebhook};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper.find('.item-details__description').length).toBe(0);
expect(container.querySelector('.item-details__description')).not.toBeInTheDocument();
});
test('Should not render any nodes as there are no filtered results', () => {
const newFilter = 'someLongText';
const props = {...baseProps, filter: newFilter};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper.getElement()).toBe(null);
expect(container.querySelector('.backstage-list__item')).not.toBeInTheDocument();
});
test('Should render a webhook item as filtered result is true', () => {
const newFilter = 'buil';
const props = {...baseProps, filter: newFilter};
const wrapper = shallow(
const {container} = renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
expect(wrapper.find('.item-details').exists()).toBe(true);
expect(container.querySelector('.item-details')).toBeInTheDocument();
});
test('Should call onRegenToken function once', () => {
test('Should call onRegenToken function once', async () => {
const newFilter = 'buil';
const newOnRegenToken = jest.fn();
const props = {...baseProps, filter: newFilter, onRegenToken: newOnRegenToken};
const wrapper = shallow(
renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
wrapper.find('.item-actions button').first().simulate('click', {preventDefault() {
return jest.fn();
}});
await userEvent.click(screen.getByText('Regenerate Token'));
expect(newOnRegenToken).toHaveBeenCalledTimes(1);
});
test('Should call onDelete function once', () => {
test('Should call onDelete function once', async () => {
const newFilter = 'buil';
const newOnDelete = jest.fn();
const props = {...baseProps, filter: newFilter, onDelete: newOnDelete};
const wrapper = shallow(
renderWithContext(
<InstalledOutgoingWebhook {...props}/>,
initialState as GlobalState,
);
wrapper.find(DeleteIntegrationLink).first().prop('onDelete')();
await userEvent.click(screen.getByText('Delete'));
expect(newOnDelete).toHaveBeenCalledTimes(1);
});
test('Should match snapshot of makeDisplayName', () => {
const wrapper = shallow(
<InstalledOutgoingWebhook {...baseProps}/>,
// Test with webhook display name
const {container: container1} = renderWithContext(
<InstalledOutgoingWebhook
{...baseProps}
outgoingWebhook={TestHelper.getOutgoingWebhookMock({...outgoingWebhook, display_name: 'hook display name'})}
/>,
initialState as GlobalState,
);
expect(container1.querySelector('.item-details__name')).toMatchSnapshot();
const instance = wrapper.instance() as InstalledOutgoingWebhook;
// Test with channel display name (no webhook display name)
const {container: container2} = renderWithContext(
<InstalledOutgoingWebhook
{...baseProps}
outgoingWebhook={TestHelper.getOutgoingWebhookMock({...outgoingWebhook, display_name: ''})}
channel={TestHelper.getChannelMock({display_name: 'channel display name'})}
/>,
initialState as GlobalState,
);
expect(container2.querySelector('.item-details__name')).toMatchSnapshot();
// displays webhook's display name
expect(instance.makeDisplayName(TestHelper.getOutgoingWebhookMock({display_name: 'hook display name'}), TestHelper.getChannelMock())).toMatchSnapshot();
// displays channel's display name
expect(instance.makeDisplayName(TestHelper.getOutgoingWebhookMock(), TestHelper.getChannelMock({display_name: 'channel display name'}))).toMatchSnapshot();
// displays a private hook
expect(instance.makeDisplayName(TestHelper.getOutgoingWebhookMock(), TestHelper.getChannelMock())).toMatchSnapshot();
// Test with no display name and no channel - displays "A Private Webhook"
const {container: container3} = renderWithContext(
<InstalledOutgoingWebhook
{...baseProps}
outgoingWebhook={TestHelper.getOutgoingWebhookMock({...outgoingWebhook, display_name: ''})}
channel={undefined as unknown as Channel}
/>,
initialState as GlobalState,
);
expect(container3.querySelector('.item-details__name')).toMatchSnapshot();
});
test('Should match result when matchesFilter is called', () => {
@@ -2,291 +2,321 @@
exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot 1`] = `
<div>
<InstalledOutgoingWebhook
canChange={true}
channel={
Object {
"create_at": 0,
"creator_id": "id",
"delete_at": 0,
"display_name": "town-square",
"group_constrained": false,
"header": "header",
"id": "mdpzfpfcxi85zkkqkzkch4b85h",
"last_post_at": 0,
"last_root_post_at": 0,
"name": "town-square",
"purpose": "purpose",
"scheme_id": "id",
"team_id": "team_id",
"type": "O",
"update_at": 0,
}
}
creator={
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "sudheer",
"id": "zaktnt8bpbgu8mb6ez9k64r7sa",
"is_bot": false,
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "system_admin system_user",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "sudheerdev",
}
}
key="7h88x419ubbyuxzs7dfwtgkfgr"
onDelete={[Function]}
onRegenToken={[Function]}
outgoingWebhook={
Object {
"0": "asdf",
"callback_urls": Array [
"http://adsfdasd.com",
],
"channel_id": "mdpzfpfcxi85zkkqkzkch4b85h",
"content_type": "application/x-www-form-urlencoded",
"create_at": 1508327769020,
"creator_id": "zaktnt8bpbgu8mb6ez9k64r7sa",
"delete_at": 0,
"description": "build status",
"display_name": "",
"id": "7h88x419ubbyuxzs7dfwtgkfgr",
"team_id": "eatxocwc3bg9ffo9xyybnj4omr",
"token": "xoxz1z7c3tgi9xhrfudn638q9r",
"trigger_when": 0,
"trigger_words": Array [
"build",
],
"update_at": 1508329149618,
}
}
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "testteamid",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<InstalledOutgoingWebhook
canChange={true}
channel={
Object {
"create_at": 0,
"creator_id": "id",
"delete_at": 0,
"display_name": "town-square",
"group_constrained": false,
"header": "header",
"id": "mdpzfpfcxi85zkkqkzkch4b85h",
"last_post_at": 0,
"last_root_post_at": 0,
"name": "town-square",
"purpose": "purpose",
"scheme_id": "id",
"team_id": "team_id",
"type": "O",
"update_at": 0,
}
}
creator={
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "sudheer",
"id": "zaktnt8bpbgu8mb6ez9k64r7sa",
"is_bot": false,
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "system_admin system_user",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "sudheerdev",
}
}
key="7h88x419ubbyuxzs7dfwtgkffr"
onDelete={[Function]}
onRegenToken={[Function]}
outgoingWebhook={
Object {
"0": "asdf",
"callback_urls": Array [
"http://adsfdasd.com",
],
"channel_id": "mdpzfpfcxi85zkkqkzkch4b85h",
"content_type": "application/x-www-form-urlencoded",
"create_at": 1508327769020,
"creator_id": "zaktnt8bpbgu8mb6ez9k64r7sa",
"delete_at": 0,
"description": "test",
"display_name": "",
"id": "7h88x419ubbyuxzs7dfwtgkffr",
"team_id": "eatxocwc3bg9ffo9xyybnj4omr",
"token": "xoxz1z7c3tgi9xhrfudn638q9r",
"trigger_when": 0,
"trigger_words": Array [
"test",
],
"update_at": 1508329149618,
}
}
team={
Object {
"allow_open_invite": false,
"allowed_domains": "",
"company_name": "",
"create_at": 0,
"delete_at": 0,
"description": "",
"display_name": "name",
"email": "",
"group_constrained": false,
"id": "testteamid",
"invite_id": "",
"name": "test",
"scheme_id": "id",
"type": "O",
"update_at": 0,
}
}
/>
<div
class="backstage-content"
>
<div
class="backstage-header"
>
<h1>
Outgoing Webhooks
</h1>
<a
class="add-link"
href="/test/integrations/outgoing_webhooks/add"
>
<button
class="btn btn-primary"
id="addOutgoingWebhook"
type="button"
>
<span>
Add Outgoing Webhook
</span>
</button>
</a>
</div>
<div
class="backstage-filters"
>
<div
class="backstage-filter__search"
>
<i
class="fa fa-search"
title="Search Icon"
/>
<input
class="form-control"
id="searchInput"
placeholder="Search Outgoing Webhooks"
type="search"
value=""
/>
</div>
</div>
<span
class="backstage-list__help"
>
Use outgoing webhooks to connect external tools to Mattermost.
<a
href="https://mattermost.com/pl/setup-outgoing-webhooks?utm_source=mattermost&utm_medium=in-product&utm_content=installed_outgoing_webhooks&uid=currentUserId&sid=&edition=team&server_version="
location="installed_outgoing_webhooks"
rel="noopener noreferrer"
target="_blank"
>
Build your own
</a>
or visit the
<a
href="https://mattermost.com/marketplace?utm_source=mattermost&utm_medium=in-product&utm_content=installed_outgoing_webhooks&uid=currentUserId&sid=&edition=team&server_version="
location="installed_outgoing_webhooks"
rel="noopener noreferrer"
target="_blank"
>
App Directory
</a>
to find self-hosted, third-party apps and integrations.
</span>
<div
class="backstage-list"
>
<div
class="backstage-list__item"
>
<div
class="item-details"
>
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong
class="item-details__name"
>
town-square
</strong>
<div
class="item-actions"
>
<button
class="style--none color--link"
>
Regenerate Token
</button>
-
<a
href="/test/integrations/outgoing_webhooks/edit?id=7h88x419ubbyuxzs7dfwtgkfgr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<div
class="item-details__row"
>
<span
class="item-details__description"
>
build status
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__content_type"
>
Content-Type: application/x-www-form-urlencoded
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__trigger-words"
>
Trigger Words: build
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__trigger-when"
>
Trigger When:
First word matches a trigger word exactly
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Token:
<code>
xoxz1z7c3tgi9xhrfudn638q9r
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by sudheerdev on Wednesday, October 18, 2017
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Callback URLs: http://adsfdasd.com
</span>
</div>
</div>
</div>
<div
class="backstage-list__item"
>
<div
class="item-details"
>
<div
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<strong
class="item-details__name"
>
town-square
</strong>
<div
class="item-actions"
>
<button
class="style--none color--link"
>
Regenerate Token
</button>
-
<a
href="/test/integrations/outgoing_webhooks/edit?id=7h88x419ubbyuxzs7dfwtgkffr"
>
Edit
</a>
-
<button>
Delete
</button>
</div>
</div>
<div
class="item-details__row"
>
<span
class="item-details__description"
>
test
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__content_type"
>
Content-Type: application/x-www-form-urlencoded
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__trigger-words"
>
Trigger Words: test
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__trigger-when"
>
Trigger When:
First word matches a trigger word exactly
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__token"
>
Token:
<code>
xoxz1z7c3tgi9xhrfudn638q9r
</code>
<button
aria-label="Copy Token"
class="btn btn-link icon-content-copy ml-2"
data-testid="copyText"
/>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by sudheerdev on Wednesday, October 18, 2017
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__url word-break--all"
>
Callback URLs: http://adsfdasd.com
</span>
</div>
</div>
</div>
</div>
<div
class="backstage-footer"
>
<div
class="backstage-footer__cell"
>
1 - 2 of 2
<button
aria-label="Previous"
class="btn btn-quaternary btn-icon btn-sm ml-2 prev disabled"
type="button"
>
<i
class="icon icon-chevron-left"
title="Previous Icon"
/>
</button>
<button
aria-label="Next"
class="btn btn-quaternary btn-icon btn-sm next disabled"
type="button"
>
<i
class="icon icon-chevron-right"
title="Next Icon"
/>
</button>
</div>
</div>
</div>
</div>
`;
exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot 2`] = `<div />`;
exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot 3`] = `
<BackstageList
addButtonId="addOutgoingWebhook"
addLink="/test/integrations/outgoing_webhooks/add"
addText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Add Outgoing Webhook"
id="installed_outgoing_webhooks.add"
/>
}
emptyText={
<Memo(MemoizedFormattedMessage)
defaultMessage="No outgoing webhooks found"
id="installed_outgoing_webhooks.empty"
/>
}
emptyTextSearch={
<Memo(MemoizedFormattedMessage)
defaultMessage="No outgoing webhooks match <b>{searchTerm}</b>"
id="installed_outgoing_webhooks.search.empty"
values={
Object {
"b": [Function],
}
}
/>
}
header={
<Memo(MemoizedFormattedMessage)
defaultMessage="Outgoing Webhooks"
id="installed_outgoing_webhooks.header"
/>
}
helpText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Use outgoing webhooks to connect external tools to Mattermost. {buildYourOwn} or visit the {appDirectory} to find self-hosted, third-party apps and integrations."
id="installed_outgoing_webhooks.help"
values={
Object {
"appDirectory": <ForwardRef
href="https://mattermost.com/marketplace"
location="installed_outgoing_webhooks"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="App Directory"
id="installed_outgoing_webhooks.help.appDirectory"
/>
</ForwardRef>,
"buildYourOwn": <ForwardRef
href="https://mattermost.com/pl/setup-outgoing-webhooks"
location="installed_outgoing_webhooks"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Build your own"
id="installed_outgoing_webhooks.help.buildYourOwn"
/>
</ForwardRef>,
}
}
/>
}
loading={true}
searchPlaceholder="Search Outgoing Webhooks"
>
<Component />
</BackstageList>
`;
@@ -1,16 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen, waitFor} from '@testing-library/react';
import React from 'react';
import type {ComponentProps} from 'react';
import type {OutgoingWebhook} from '@mattermost/types/integrations';
import InstalledOutgoingWebhooks from 'components/integrations/installed_outgoing_webhooks/installed_outgoing_webhooks';
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
jest.mock('components/integrations/delete_integration_link', () => {
const ReactMock = require('react'); // eslint-disable-line @typescript-eslint/no-var-requires, global-require
return {
__esModule: true,
default: (props: {onDelete: () => void}) => ReactMock.createElement('button', {onClick: props.onDelete}, 'Delete'),
};
});
describe('components/integrations/InstalledOutgoingWebhooks', () => {
const teamId = 'testteamid';
const team = TestHelper.getTeamMock({
@@ -65,7 +73,18 @@ describe('components/integrations/InstalledOutgoingWebhooks', () => {
} as unknown as OutgoingWebhook,
];
const defaultProps: ComponentProps<typeof InstalledOutgoingWebhooks> = {
const initialState = {
entities: {
general: {
config: {},
},
users: {
currentUserId: 'currentUserId',
},
},
};
const defaultProps = {
outgoingWebhooks,
teamId,
team,
@@ -81,38 +100,70 @@ describe('components/integrations/InstalledOutgoingWebhooks', () => {
canManageOthersWebhooks: true,
};
test('should match snapshot', () => {
const wrapper = shallow<InstalledOutgoingWebhooks>(
test('should match snapshot', async () => {
const {container} = renderWithContext(
<InstalledOutgoingWebhooks
{...defaultProps}
/>,
initialState,
);
expect(shallow(<div>{wrapper.instance().outgoingWebhooks('town')}</div>)).toMatchSnapshot();
expect(shallow(<div>{wrapper.instance().outgoingWebhooks('ZZZ')}</div>)).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(screen.getByText('build status')).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
test('should call regenOutgoingHookToken function', () => {
const wrapper = shallow<InstalledOutgoingWebhooks>(
test('should call regenOutgoingHookToken function', async () => {
const regenOutgoingHookToken = jest.fn();
const props = {
...defaultProps,
actions: {
...defaultProps.actions,
regenOutgoingHookToken,
},
};
renderWithContext(
<InstalledOutgoingWebhooks
{...defaultProps}
{...props}
/>,
initialState,
);
wrapper.instance().regenOutgoingWebhookToken(outgoingWebhooks[0]);
expect(defaultProps.actions.regenOutgoingHookToken).toHaveBeenCalledTimes(1);
expect(defaultProps.actions.regenOutgoingHookToken).toHaveBeenCalledWith(outgoingWebhooks[0].id);
await waitFor(() => {
expect(screen.getAllByRole('button', {name: 'Regenerate Token'})).toHaveLength(2);
});
await userEvent.click(screen.getAllByRole('button', {name: 'Regenerate Token'})[0]);
expect(regenOutgoingHookToken).toHaveBeenCalledTimes(1);
expect(regenOutgoingHookToken).toHaveBeenCalledWith(outgoingWebhooks[0].id);
});
test('should call removeOutgoingHook function', () => {
const wrapper = shallow<InstalledOutgoingWebhooks>(
test('should call removeOutgoingHook function', async () => {
const removeOutgoingHook = jest.fn();
const props = {
...defaultProps,
actions: {
...defaultProps.actions,
removeOutgoingHook,
},
};
renderWithContext(
<InstalledOutgoingWebhooks
{...defaultProps}
{...props}
/>,
initialState,
);
wrapper.instance().removeOutgoingHook(outgoingWebhooks[1]);
expect(defaultProps.actions.removeOutgoingHook).toHaveBeenCalledTimes(1);
expect(defaultProps.actions.removeOutgoingHook).toHaveBeenCalledWith(outgoingWebhooks[1].id);
expect(defaultProps.actions.removeOutgoingHook).toHaveBeenCalled();
await waitFor(() => {
expect(screen.getAllByRole('button', {name: 'Delete'})).toHaveLength(2);
});
await userEvent.click(screen.getAllByRole('button', {name: 'Delete'})[1]);
expect(removeOutgoingHook).toHaveBeenCalledTimes(1);
expect(removeOutgoingHook).toHaveBeenCalledWith(outgoingWebhooks[1].id);
});
});
@@ -1,140 +1,93 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/integrations/InstalledOutgoingOAuthConnection should match snapshot 1`] = `
<div
className="backstage-list__item"
>
<div>
<div
className="item-details"
class="backstage-list__item"
>
<div
className="item-details__row d-flex flex-column flex-md-row justify-content-between"
class="item-details"
>
<strong
className="item-details__name"
>
My OAuth Connection
</strong>
<div
className="item-actions"
class="item-details__row d-flex flex-column flex-md-row justify-content-between"
>
<Link
to="/team_name/integrations/outgoing-oauth2-connections/edit?id=someid"
<strong
class="item-details__name"
>
<MemoizedFormattedMessage
defaultMessage="Edit"
id="installed_integrations.edit"
/>
</Link>
-
<Connect(DeleteIntegrationLink)
modalMessage={
<Memo(MemoizedFormattedMessage)
defaultMessage="Deleting this connection will break any integrations using it"
id="installed_outgoing_oauth_connections.delete.wanring"
/>
}
onDelete={[Function]}
subtitleText={
<Memo(MemoizedFormattedMessage)
defaultMessage="Are you sure you want to delete {connectionName}?"
id="installed_outgoing_oauth_connections.delete.confirm"
values={
Object {
"connectionName": <strong>
My OAuth Connection
</strong>,
}
}
/>
}
/>
</div>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
>
<MemoizedFormattedMessage
defaultMessage="Client ID: "
id="installed_integrations.client_id"
/>
<strong>
<code>
someid
</code>
My OAuth Connection
</strong>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__token"
<div
class="item-actions"
>
<a
href="/team_name/integrations/outgoing-oauth2-connections/edit?id=someid"
>
Edit
</a>
-
<button
data-testid="delete-integration-link"
>
Delete
</button>
</div>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Client Secret: ********"
id="installed_outgoing_oauth_connections.client_secret"
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__token"
>
Client ID:
<strong>
<code>
someid
</code>
</strong>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Audience URLs: {urls}"
id="installed_integrations.audience_urls"
values={
Object {
"urls": <code>
https://myaudience.com
</code>,
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__url word-break--all"
<span
class="item-details__token"
>
Client Secret: ********
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Token URL: {url}"
id="installed_integrations.token_url"
values={
Object {
"url": <code>
https://tokenurl.com
</code>,
}
}
/>
</span>
</div>
<div
className="item-details__row"
>
<span
className="item-details__creation"
<span
class="item-details__url word-break--all"
>
Audience URLs:
<code>
https://myaudience.com
</code>
</span>
</div>
<div
class="item-details__row"
>
<MemoizedFormattedMessage
defaultMessage="Created by {creator} on {createAt, date, full}"
id="installed_integrations.creation"
values={
Object {
"createAt": 1501365458934,
"creator": "somename",
}
}
/>
</span>
<span
class="item-details__url word-break--all"
>
Token URL:
<code>
https://tokenurl.com
</code>
</span>
</div>
<div
class="item-details__row"
>
<span
class="item-details__creation"
>
Created by somename on Saturday, July 29, 2017
</span>
</div>
</div>
</div>
</div>
@@ -1,15 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
import DeleteIntegrationLink from 'components/integrations/delete_integration_link';
import type {InstalledOutgoingOAuthConnectionProps} from 'components/integrations/outgoing_oauth_connections/installed_outgoing_oauth_connection';
import InstalledOutgoingOAuthConnection from 'components/integrations/outgoing_oauth_connections/installed_outgoing_oauth_connection';
import {renderWithContext} from 'tests/react_testing_utils';
jest.mock('components/integrations/delete_integration_link', () => {
return function MockDeleteIntegrationLink(props: {onDelete: () => void}) {
return (
<button
data-testid='delete-integration-link'
onClick={props.onDelete}
>
{'Delete'}
</button>
);
};
});
describe('components/integrations/InstalledOutgoingOAuthConnection', () => {
const team = {name: 'team_name'};
const outgoingOAuthConnection: OutgoingOAuthConnection = {
@@ -34,22 +49,23 @@ describe('components/integrations/InstalledOutgoingOAuthConnection', () => {
};
test('should match snapshot', () => {
const props = {...baseProps};
const wrapper = shallow(
<InstalledOutgoingOAuthConnection {...props}/>,
const {container} = renderWithContext(
<InstalledOutgoingOAuthConnection {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should have called props.onDelete on handleDelete ', () => {
test('should have called props.onDelete on handleDelete ', async () => {
const newOnDelete = jest.fn();
const props = {...baseProps, team, onDelete: newOnDelete};
const wrapper = shallow(
renderWithContext(
<InstalledOutgoingOAuthConnection {...props}/>,
);
expect(wrapper.find(DeleteIntegrationLink).exists()).toBe(true);
wrapper.find(DeleteIntegrationLink).props().onDelete();
const deleteButton = screen.getByTestId('delete-integration-link');
expect(deleteButton).toBeInTheDocument();
await userEvent.click(deleteButton);
expect(newOnDelete).toHaveBeenCalled();
expect(newOnDelete).toHaveBeenCalledWith(outgoingOAuthConnection);
});
@@ -1,37 +1,63 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/cloud_start_trial_btn/cloud_start_trial_btn should match snapshot 1`] = `
<ContextProvider
value={
Object {
"getServerState": undefined,
"identityFunctionCheck": "once",
"stabilityCheck": "once",
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Object {
"addNestedSub": [Function],
"getListeners": [Function],
"handleChangeWrapper": [Function],
"isSubscribed": [Function],
"notifyNestedSubs": [Function],
"trySubscribe": [Function],
"tryUnsubscribe": [Function],
},
}
}
>
<InviteAs
canInviteGuests={true}
inviteType="MEMBER"
setInviteAs={[MockFunction]}
titleClass="title"
/>
</ContextProvider>
<div>
<div
class="InviteAs"
>
<div
class="title"
>
Invite as
</div>
<div>
<div
class="radio-list"
data-testid=""
>
<div
class="radio"
>
<label
class="selected"
>
<input
checked=""
data-testid="inviteMembersLink"
name="invite-as"
type="radio"
value="MEMBER"
/>
Member
</label>
</div>
<div
class="radio"
>
<label
class=""
>
<input
data-testid="inviteGuestLink"
name="invite-as"
type="radio"
value="GUEST"
/>
<span
class="InviteAs__label "
>
Guest
<span
class="InviteAs__label--parenthetical"
>
-
limited to select channels and teams
</span>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
`;
@@ -1,14 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {screen} from '@testing-library/react';
import React from 'react';
import {Provider} from 'react-redux';
import RadioGroup from 'components/common/radio_group';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {renderWithContext} from 'tests/react_testing_utils';
import {CloudProducts} from 'utils/constants';
import InviteAs, {InviteType} from './invite_as';
@@ -18,8 +14,13 @@ jest.mock('mattermost-redux/selectors/entities/users', () => ({
isCurrentUserSystemAdmin: () => true,
}));
jest.mock('mattermost-redux/actions/admin', () => ({
...jest.requireActual('mattermost-redux/actions/admin') as typeof import('mattermost-redux/actions/admin'),
getPrevTrialLicense: () => ({type: 'MOCK_GET_PREV_TRIAL_LICENSE'}),
}));
describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
const THIRTY_DAYS = (60 * 60 * 24 * 30 * 1000); // in milliseconds
const THIRTY_DAYS = (60 * 60 * 24 * 30 * 1000);
const subscriptionCreateAt = Date.now();
const subscriptionEndAt = subscriptionCreateAt + THIRTY_DAYS;
@@ -61,28 +62,26 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
};
const store = mockStore(state);
test('should match snapshot', () => {
const wrapper = shallow(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
const {container} = renderWithContext(
<InviteAs {...props}/>,
state,
{useMockedStore: true},
);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('shows the radio buttons', () => {
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
state,
{useMockedStore: true},
);
expect(wrapper.find(RadioGroup).length).toBe(1);
expect(screen.getAllByRole('radio')).toHaveLength(2);
});
test('guest radio-button is disabled and shows the badge guest restricted feature to invite guest when is NOT free trial for cloud', () => {
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -120,22 +119,20 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(true);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).toBeDisabled();
const badgeText = wrapper.find('.Tag span.tag-text').text();
expect(badgeText).toBe('Professional feature- try it out free');
expect(screen.getByText('Professional feature- try it out free')).toBeInTheDocument();
});
test('restricted badge shows "Upgrade" for cloud post trial', () => {
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -173,22 +170,20 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(true);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).toBeDisabled();
const badgeText = wrapper.find('.Tag span.tag-text').text();
expect(badgeText).toBe('Upgrade');
expect(screen.getByText('Upgrade')).toBeInTheDocument();
});
test('guest radio-button is disabled and shows the badge guest restricted feature to invite guest when is NOT free trial for self hosted starter', () => {
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -212,22 +207,20 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(true);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).toBeDisabled();
const badgeText = wrapper.find('.Tag span.tag-text').text();
expect(badgeText).toBe('Professional feature- try it out free');
expect(screen.getByText('Professional feature- try it out free')).toBeInTheDocument();
});
test('restricted badge shows "Upgrade" for self hosted starter post trial', () => {
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -251,22 +244,20 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(true);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).toBeDisabled();
const badgeText = wrapper.find('.Tag span.tag-text').text();
expect(badgeText).toBe('Upgrade');
expect(screen.getByText('Upgrade')).toBeInTheDocument();
});
test('shows the badge guest highligh feature to invite guest when IS FREE trial for cloud', () => {
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -297,22 +288,20 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(false);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).not.toBeDisabled();
const badgeText = wrapper.find('.Tag span.tag-text').text();
expect(badgeText).toBe('Professional feature');
expect(screen.getByText('Professional feature')).toBeInTheDocument();
});
test('shows the badge guest highligh feature to invite guest when IS FREE trial for self hosted starter', () => {
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -337,18 +326,16 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(false);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).not.toBeDisabled();
const badgeText = wrapper.find('.Tag span.tag-text').text();
expect(badgeText).toBe('Professional feature');
expect(screen.getByText('Professional feature')).toBeInTheDocument();
});
test('guest radio-button is disabled when canInviteGuests prop is false', () => {
@@ -396,15 +383,14 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(paidState);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...propsWithCanInviteGuestsFalse}/>
</Provider>,
renderWithContext(
<InviteAs {...propsWithCanInviteGuestsFalse}/>,
paidState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(true);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).toBeDisabled();
});
test('guest radio-button is enabled when canInviteGuests prop is true and other conditions allow it', () => {
@@ -452,20 +438,19 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(paidState);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...propsWithCanInviteGuestsTrue}/>
</Provider>,
renderWithContext(
<InviteAs {...propsWithCanInviteGuestsTrue}/>,
paidState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(false);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).not.toBeDisabled();
});
test('guest radio-button is disabled when canInviteGuests prop is undefined and defaults to system behavior', () => {
// Test with starter plan where guests should be disabled by default
const state = {
const testState = {
entities: {
admin: {
prevTrialLicense: {
@@ -503,14 +488,13 @@ describe('components/cloud_start_trial_btn/cloud_start_trial_btn', () => {
},
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<InviteAs {...props}/>
</Provider>,
renderWithContext(
<InviteAs {...props}/>,
testState,
{useMockedStore: true},
);
const guestRadioButton = wrapper.find('input[value="GUEST"]');
expect(guestRadioButton.props().disabled).toBe(true);
const guestRadioButton = screen.getByDisplayValue('GUEST');
expect(guestRadioButton).toBeDisabled();
});
});
@@ -1,21 +1,61 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/post_view/post_view should match snapshot for channel loading 1`] = `
<div
id="post-list"
>
<LoadingScreen
centered={true}
/>
<div>
<div
id="post-list"
>
<div
class="loading-screen loading-screen--in-middle"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
</div>
`;
exports[`components/post_view/post_view should match snapshot for loaderForChangeOfPostsChunk 1`] = `
<div
id="post-list"
>
<LoadingScreen
centered={true}
/>
<div>
<div
id="post-list"
>
<div
class="loading-screen loading-screen--in-middle"
style="position: relative;"
>
<div
class="loading__content"
>
<p>
Loading
</p>
<div
class="round round-1"
/>
<div
class="round round-2"
/>
<div
class="round round-3"
/>
</div>
</div>
</div>
</div>
`;
@@ -1,18 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {PostAction} from '@mattermost/types/integration_actions';
import type {MessageAttachment as MessageAttachmentType} from '@mattermost/types/message_attachments';
import type {PostImage} from '@mattermost/types/posts';
import ExternalImage from 'components/external_image';
import MessageAttachment from 'components/post_view/message_attachments/message_attachment/message_attachment';
import {act, renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {Constants} from 'utils/constants';
jest.mock('components/external_image', () => {
return jest.fn((props: any) => <>{props.children(props.src)}</>);
});
jest.mock('components/markdown', () => {
return jest.fn((props: any) => <div data-testid='markdown-mock'>{props.message}</div>);
});
jest.mock('components/size_aware_image', () => {
return jest.fn((props: any) => (
<img
data-testid='size-aware-image-mock'
className={props.className}
src={props.src}
onClick={(e: React.MouseEvent<HTMLImageElement>) => props.onClick?.(e, props.src)}
/>
));
});
jest.mock('../action_button', () => {
return jest.fn((props: any) => (
<button
data-testid='action-button-mock'
data-action-id={props.action.id}
data-action-cookie={props.action.cookie}
onClick={(e: React.MouseEvent) => props.handleAction(e, props.action.options)}
>
{props.action.name}
</button>
));
});
jest.mock('../action_menu', () => {
return jest.fn((props: any) => (
<div data-testid='action-menu-mock'>{props.action.name}</div>
));
});
describe('components/post_view/MessageAttachment', () => {
const attachment = {
pretext: 'pretext',
@@ -50,26 +87,52 @@ describe('components/post_view/MessageAttachment', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<MessageAttachment {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<MessageAttachment {...baseProps}/>);
expect(container).toMatchSnapshot();
});
test('should change checkOverflow state on handleHeightReceived change', () => {
const wrapper = shallow<MessageAttachment>(<MessageAttachment {...baseProps}/>);
const instance = wrapper.instance();
// Mock Markdown to capture imageProps
const MarkdownMock = jest.requireMock('components/markdown');
MarkdownMock.mockClear();
wrapper.setState({checkOverflow: 0});
instance.handleHeightReceived(1);
expect(wrapper.state('checkOverflow')).toEqual(1);
renderWithContext(<MessageAttachment {...baseProps}/>);
instance.handleHeightReceived(0);
expect(wrapper.state('checkOverflow')).toEqual(1);
// Find the Markdown call that has imageProps (the attachment text one)
const callWithImageProps = MarkdownMock.mock.calls.find(
(call: any) => call[0].imageProps,
);
expect(callWithImageProps).toBeDefined();
const imageProps = callWithImageProps[0].imageProps;
const callCountBefore = MarkdownMock.mock.calls.length;
// Call onImageLoaded with height > 0 (triggers checkPostOverflow)
act(() => {
imageProps.onImageLoaded(1);
});
// Component should re-render
expect(MarkdownMock.mock.calls.length).toBeGreaterThan(callCountBefore);
// Call onImageLoaded with height 0 (should NOT trigger checkPostOverflow)
const callCountAfter = MarkdownMock.mock.calls.length;
act(() => {
imageProps.onImageLoaded(0);
});
// No additional re-render
expect(MarkdownMock.mock.calls.length).toEqual(callCountAfter);
});
test('should match value on renderPostActions', () => {
let wrapper = shallow<MessageAttachment>(<MessageAttachment {...baseProps}/>);
expect(wrapper.instance().renderPostActions()).toMatchSnapshot();
// Without actions - no action buttons should render
const {container, unmount} = renderWithContext(<MessageAttachment {...baseProps}/>);
expect(container.querySelector('.attachment-actions')).not.toBeInTheDocument();
unmount();
// With actions
const newAttachment = {
...attachment,
actions: [
@@ -80,12 +143,16 @@ describe('components/post_view/MessageAttachment', () => {
};
const props = {...baseProps, attachment: newAttachment};
const {container: container2} = renderWithContext(<MessageAttachment {...props}/>);
wrapper = shallow(<MessageAttachment {...props}/>);
expect(wrapper.instance().renderPostActions()).toMatchSnapshot();
// Should render action buttons and action menu
expect(container2.querySelector('.attachment-actions')).toBeInTheDocument();
expect(screen.getAllByTestId('action-button-mock')).toHaveLength(2);
expect(screen.getByTestId('action-menu-mock')).toBeInTheDocument();
expect(container2.querySelector('.attachment-actions')).toMatchSnapshot();
});
test('should call actions.doPostActionWithCookie on handleAction', () => {
test('should call actions.doPostActionWithCookie on handleAction', async () => {
const promise = Promise.resolve({data: 123});
const doPostActionWithCookie = jest.fn(() => promise);
const openModal = jest.fn();
@@ -95,33 +162,48 @@ describe('components/post_view/MessageAttachment', () => {
actions: [{id: actionId, name: 'action_name_1', cookie: 'cookie-contents'}] as PostAction[],
};
const props = {...baseProps, actions: {doPostActionWithCookie, openModal}, attachment: newAttachment};
const wrapper = shallow<MessageAttachment>(<MessageAttachment {...props}/>);
expect(wrapper).toMatchSnapshot();
wrapper.instance().handleAction({
preventDefault: () => {}, // eslint-disable-line no-empty-function
currentTarget: {getAttribute: () => {
return 'attr_some_value';
}} as any,
} as React.MouseEvent, []);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(container).toMatchSnapshot();
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.click(screen.getByTestId('action-button-mock'));
expect(doPostActionWithCookie).toHaveBeenCalledTimes(1);
expect(doPostActionWithCookie).toHaveBeenCalledWith(props.postId, 'attr_some_value', 'attr_some_value');
expect(doPostActionWithCookie).toHaveBeenCalledWith(props.postId, actionId, 'cookie-contents');
});
test('should call openModal when showModal is called', () => {
const props = {...baseProps, src: 'https://example.com/image.png'};
const wrapper = shallow<MessageAttachment>(
<MessageAttachment {...props}/>,
);
test('should call openModal when showModal is called', async () => {
const props = {
...baseProps,
attachment: {
...attachment,
image_url: 'https://example.com/image.png',
} as MessageAttachmentType,
imagesMetadata: {
...baseProps.imagesMetadata,
'https://example.com/image.png': {height: 200, width: 200} as PostImage,
},
};
renderWithContext(<MessageAttachment {...props}/>);
// Click on the image to trigger showModal
const {userEvent} = await import('tests/react_testing_utils');
const images = screen.getAllByTestId('size-aware-image-mock');
const attachmentImage = images.find((img) => img.getAttribute('src') === 'https://example.com/image.png');
expect(attachmentImage).toBeDefined();
await userEvent.click(attachmentImage!);
wrapper.instance().showModal({preventDefault: () => {}} as unknown as React.KeyboardEvent<HTMLImageElement> | React.MouseEvent<HTMLElement, MouseEvent>, 'https://example.com/image.png');
expect(props.actions.openModal).toHaveBeenCalledTimes(1);
});
test('should match value on getFieldsTable', () => {
let wrapper = shallow<MessageAttachment>(<MessageAttachment {...baseProps}/>);
expect(wrapper.instance().getFieldsTable()).toMatchSnapshot();
// Without fields - no field table should render
const {container, unmount} = renderWithContext(<MessageAttachment {...baseProps}/>);
expect(container.querySelector('.attachment-fields')).not.toBeInTheDocument();
unmount();
// With fields
const newAttachment = {
...attachment,
fields: [
@@ -131,12 +213,17 @@ describe('components/post_view/MessageAttachment', () => {
};
const props = {...baseProps, attachment: newAttachment};
const {container: container2} = renderWithContext(<MessageAttachment {...props}/>);
wrapper = shallow(<MessageAttachment {...props}/>);
expect(wrapper.instance().getFieldsTable()).toMatchSnapshot();
const fieldTables = container2.querySelectorAll('.attachment-fields');
expect(fieldTables.length).toBeGreaterThan(0);
expect(container2.querySelector('.attachment-fields')!.parentElement).toMatchSnapshot();
});
test('should use ExternalImage for images', () => {
const ExternalImageMock = jest.requireMock('components/external_image');
ExternalImageMock.mockClear();
const props = {
...baseProps,
attachment: {
@@ -150,13 +237,16 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper.find(ExternalImage)).toHaveLength(4);
expect(wrapper.find(ExternalImage).find({src: props.attachment.author_icon}).exists()).toBe(true);
expect(wrapper.find(ExternalImage).find({src: props.attachment.image_url}).exists()).toBe(true);
expect(wrapper.find(ExternalImage).find({src: props.attachment.footer_icon}).exists()).toBe(true);
expect(wrapper.find(ExternalImage).find({src: props.attachment.thumb_url}).exists()).toBe(true);
// ExternalImage should have been called 4 times
expect(ExternalImageMock).toHaveBeenCalledTimes(4);
const srcValues = ExternalImageMock.mock.calls.map((call: any) => call[0].src);
expect(srcValues).toContain(props.attachment.author_icon);
expect(srcValues).toContain(props.attachment.image_url);
expect(srcValues).toContain(props.attachment.footer_icon);
expect(srcValues).toContain(props.attachment.thumb_url);
});
test('should match snapshot when the attachment has an emoji in the title', () => {
@@ -167,9 +257,9 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when the attachment hasn\'t any emojis in the title', () => {
@@ -180,9 +270,9 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when the attachment has a link in the title', () => {
@@ -193,9 +283,9 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when no footer is provided (even if footer_icon is provided)', () => {
@@ -207,9 +297,9 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot when the footer is truncated', () => {
@@ -221,9 +311,9 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot and render a field with a number value', () => {
@@ -240,9 +330,9 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(wrapper.find('.attachment-field')).toMatchSnapshot();
expect(container.querySelector('.attachment-field')).toMatchSnapshot();
});
test('should not render content box if there is no content', () => {
@@ -253,11 +343,11 @@ describe('components/post_view/MessageAttachment', () => {
} as MessageAttachmentType,
};
const wrapper = shallow(<MessageAttachment {...props}/>);
expect(wrapper.find('.attachment')).toMatchSnapshot();
const {container} = renderWithContext(<MessageAttachment {...props}/>);
expect(container.querySelector('.attachment')).toMatchSnapshot();
});
test('should handle action errors and display error message', (done) => {
test('should handle action errors and display error message', async () => {
const errorMessage = 'Action failed to execute';
const doPostActionWithCookie = jest.fn().mockResolvedValue({
error: {message: errorMessage},
@@ -268,31 +358,23 @@ describe('components/post_view/MessageAttachment', () => {
actions: [{id: actionId, name: 'action_name_1', cookie: 'cookie-contents'}] as PostAction[],
};
const props = {...baseProps, actions: {doPostActionWithCookie, openModal: jest.fn()}, attachment: newAttachment};
const wrapper = shallow<MessageAttachment>(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
// Initially no error should be shown
expect(wrapper.find('.has-error')).toHaveLength(0);
expect(container.querySelector('.has-error')).not.toBeInTheDocument();
// Trigger action
wrapper.instance().handleAction({
preventDefault: () => {},
currentTarget: {getAttribute: () => 'attr_value'} as any,
} as React.MouseEvent, []);
// Trigger action by clicking the button
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.click(screen.getByTestId('action-button-mock'));
// Wait for promise to resolve
process.nextTick(() => {
// Update wrapper to get latest state
wrapper.update();
// Error should now be displayed
expect(wrapper.state('actionError')).toBe(errorMessage);
expect(wrapper.find('.has-error')).toHaveLength(1);
expect(wrapper.find('.control-label').text()).toBe(errorMessage);
done();
// Error should now be displayed
await waitFor(() => {
expect(container.querySelector('.has-error')).toBeInTheDocument();
});
expect(container.querySelector('.control-label')?.textContent).toBe(errorMessage);
});
test('should handle promise rejection errors', (done) => {
test('should handle promise rejection errors', async () => {
const errorMessage = 'Network error occurred';
const doPostActionWithCookie = jest.fn().mockRejectedValue(new Error(errorMessage));
const actionId = 'action_id_1';
@@ -301,59 +383,53 @@ describe('components/post_view/MessageAttachment', () => {
actions: [{id: actionId, name: 'action_name_1', cookie: 'cookie-contents'}] as PostAction[],
};
const props = {...baseProps, actions: {doPostActionWithCookie, openModal: jest.fn()}, attachment: newAttachment};
const wrapper = shallow<MessageAttachment>(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
// Initially no error should be shown
expect(wrapper.find('.has-error')).toHaveLength(0);
expect(container.querySelector('.has-error')).not.toBeInTheDocument();
// Trigger action
wrapper.instance().handleAction({
preventDefault: () => {},
currentTarget: {getAttribute: () => 'attr_value'} as any,
} as React.MouseEvent, []);
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.click(screen.getByTestId('action-button-mock'));
// Wait for promise to reject and catch to execute
process.nextTick(() => {
// Update wrapper to get latest state
wrapper.update();
// Error should now be displayed
expect(wrapper.state('actionError')).toBe(errorMessage);
expect(wrapper.find('.has-error')).toHaveLength(1);
expect(wrapper.find('.control-label').text()).toBe(errorMessage);
done();
// Error should now be displayed
await waitFor(() => {
expect(container.querySelector('.has-error')).toBeInTheDocument();
});
expect(container.querySelector('.control-label')?.textContent).toBe(errorMessage);
});
test('should clear previous errors when new action is triggered', (done) => {
const doPostActionWithCookie = jest.fn().mockResolvedValue({data: 'success'});
test('should clear previous errors when new action is triggered', async () => {
const doPostActionWithCookie = jest.fn().
mockResolvedValueOnce({error: {message: 'Previous error'}}).
mockResolvedValueOnce({data: 'success'});
const actionId = 'action_id_1';
const newAttachment = {
...attachment,
actions: [{id: actionId, name: 'action_name_1', cookie: 'cookie-contents'}] as PostAction[],
};
const props = {...baseProps, actions: {doPostActionWithCookie, openModal: jest.fn()}, attachment: newAttachment};
const wrapper = shallow<MessageAttachment>(<MessageAttachment {...props}/>);
const {container} = renderWithContext(<MessageAttachment {...props}/>);
// Set initial error state
wrapper.setState({actionError: 'Previous error'});
expect(wrapper.state('actionError')).toBe('Previous error');
// Trigger first action that causes an error
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.click(screen.getByTestId('action-button-mock'));
// Trigger new action
wrapper.instance().handleAction({
preventDefault: () => {},
currentTarget: {getAttribute: () => 'attr_value'} as any,
} as React.MouseEvent, []);
// Wait for error to appear
await waitFor(() => {
expect(container.querySelector('.has-error')).toBeInTheDocument();
});
// Wait for promise to resolve
process.nextTick(() => {
// Error should be cleared on successful action
expect(wrapper.state('actionError')).toBeNull();
done();
// Trigger new action that succeeds
await userEvent.click(screen.getByTestId('action-button-mock'));
// Error should be cleared on successful action
await waitFor(() => {
expect(container.querySelector('.has-error')).not.toBeInTheDocument();
});
});
test('should render error message with default text when no error message provided', (done) => {
test('should render error message with default text when no error message provided', async () => {
const doPostActionWithCookie = jest.fn().mockResolvedValue({
error: {}, // Error object without message
});
@@ -363,30 +439,15 @@ describe('components/post_view/MessageAttachment', () => {
actions: [{id: actionId, name: 'action_name_1', cookie: 'cookie-contents'}] as PostAction[],
};
const props = {...baseProps, actions: {doPostActionWithCookie, openModal: jest.fn()}, attachment: newAttachment};
const wrapper = shallow<MessageAttachment>(<MessageAttachment {...props}/>);
renderWithContext(<MessageAttachment {...props}/>);
// Trigger action
wrapper.instance().handleAction({
preventDefault: () => {},
currentTarget: {getAttribute: () => 'attr_value'} as any,
} as React.MouseEvent, []);
const {userEvent} = await import('tests/react_testing_utils');
await userEvent.click(screen.getByTestId('action-button-mock'));
// Wait for promise to resolve
process.nextTick(() => {
// Update wrapper to get latest state
wrapper.update();
// Should show default error message as FormattedMessage component
const actionError = wrapper.state('actionError');
expect(actionError).not.toBeNull();
expect(React.isValidElement(actionError)).toBe(true);
// Verify FormattedMessage props
if (React.isValidElement(actionError)) {
expect(actionError.props.id).toBe('post.message_attachment.action_failed');
expect(actionError.props.defaultMessage).toBe('Action failed to execute');
}
done();
// Should show default error message
await waitFor(() => {
expect(screen.getByText('Action failed to execute')).toBeInTheDocument();
});
});
});
@@ -1,206 +1,195 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/post_view/PostAttachment should match snapshot 1`] = `
<Connect(ShowMore)
checkOverflow={0}
text="post message"
>
<div>
<div
className="post-message__text"
dir="auto"
id="postMessageText_post_id"
onClick={[Function]}
class="post-message post-message--collapsed"
>
<Connect(PostMarkdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
isRHS={false}
message="post message"
options={Object {}}
post={
Object {
"id": "post_id",
"message": "post message",
}
}
/>
<div
class="post-message__text-container"
style="max-height: 600px;"
>
<div
class="post-message__text"
dir="auto"
id="postMessageText_post_id"
>
<div
data-testid="post-markdown"
>
post message
</div>
</div>
<div
data-testid="pluggable-mock"
/>
</div>
</div>
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
/>
</Connect(ShowMore)>
</div>
`;
exports[`components/post_view/PostAttachment should match snapshot, on Show Less 1`] = `
<Connect(ShowMore)
checkOverflow={0}
text="post message"
>
<div>
<div
className="post-message__text"
dir="auto"
id="postMessageText_post_id"
onClick={[Function]}
class="post-message post-message--expanded post-message--overflow"
>
<Connect(PostMarkdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
isRHS={false}
message="post message"
options={Object {}}
post={
Object {
"id": "post_id",
"message": "post message",
}
}
/>
<div
class="post-message__text-container"
style=""
>
<div
class="post-message__text"
dir="auto"
id="postMessageText_post_id"
>
<div
data-testid="post-markdown"
>
post message
</div>
</div>
<div
data-testid="pluggable-mock"
/>
</div>
<div
class="post-collapse"
>
<div
class="post-collapse__show-more"
>
<div
class="post-collapse__show-more-line"
/>
<button
class="post-collapse__show-more-button"
id="showMoreButton"
>
<span
class="fa fa-angle-up"
/>
Show less
</button>
<div
class="post-collapse__show-more-line"
/>
</div>
</div>
</div>
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
/>
</Connect(ShowMore)>
</div>
`;
exports[`components/post_view/PostAttachment should match snapshot, on Show More 1`] = `
<Connect(ShowMore)
checkOverflow={0}
text="post message"
>
<div>
<div
className="post-message__text"
dir="auto"
id="postMessageText_post_id"
onClick={[Function]}
class="post-message post-message--collapsed post-message--overflow"
>
<Connect(PostMarkdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
isRHS={false}
message="post message"
options={Object {}}
post={
Object {
"id": "post_id",
"message": "post message",
}
}
/>
<div
class="post-message__text-container"
style="max-height: 1px;"
>
<div
class="post-message__text"
dir="auto"
id="postMessageText_post_id"
>
<div
data-testid="post-markdown"
>
post message
</div>
</div>
<div
data-testid="pluggable-mock"
/>
</div>
<div
class="post-collapse"
>
<div
class="post-collapse__show-more"
>
<div
class="post-collapse__show-more-line"
/>
<button
class="post-collapse__show-more-button"
id="showMoreButton"
>
<span
class="fa fa-angle-down"
/>
Show more
</button>
<div
class="post-collapse__show-more-line"
/>
</div>
</div>
</div>
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
/>
</Connect(ShowMore)>
</div>
`;
exports[`components/post_view/PostAttachment should match snapshot, on deleted post 1`] = `
<p>
<MemoizedFormattedMessage
defaultMessage="(message deleted)"
id="post_body.deleted"
/>
</p>
`;
exports[`components/post_view/PostAttachment should match snapshot, on deleted post 2`] = `
<p>
<Memo(MemoizedFormattedMessage)
defaultMessage="(message deleted)"
id="post_body.deleted"
/>
</p>
<div>
<p>
(message deleted)
</p>
</div>
`;
exports[`components/post_view/PostAttachment should match snapshot, on edited post 1`] = `
<Connect(ShowMore)
checkOverflow={0}
text="post message"
>
<div>
<div
className="post-message__text"
dir="auto"
id="postMessageText_post_id"
onClick={[Function]}
class="post-message post-message--collapsed"
>
<Connect(PostMarkdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
isRHS={false}
message="post message"
options={Object {}}
post={
Object {
"edit_at": 1,
"id": "post_id",
"message": "post message",
}
}
/>
<div
class="post-message__text-container"
style="max-height: 600px;"
>
<div
class="post-message__text"
dir="auto"
id="postMessageText_post_id"
>
<div
data-testid="post-markdown"
>
post message
</div>
</div>
<div
data-testid="pluggable-mock"
/>
</div>
</div>
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
/>
</Connect(ShowMore)>
</div>
`;
exports[`components/post_view/PostAttachment should match snapshot, on ephemeral post 1`] = `
<Connect(ShowMore)
checkOverflow={0}
text="post message"
>
<div>
<div
className="post-message__text"
dir="auto"
id="postMessageText_post_id"
onClick={[Function]}
class="post-message post-message--collapsed"
>
<Connect(PostMarkdown)
imageProps={
Object {
"onImageHeightChanged": [Function],
"onImageLoaded": [Function],
}
}
isRHS={false}
message="post message"
options={Object {}}
post={
Object {
"id": "post_id",
"message": "post message",
"type": "system_ephemeral",
}
}
/>
<div
class="post-message__text-container"
style="max-height: 600px;"
>
<div
class="post-message__text"
dir="auto"
id="postMessageText_post_id"
>
<div
data-testid="post-markdown"
>
post message
</div>
</div>
<div
data-testid="pluggable-mock"
/>
</div>
</div>
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
/>
</Connect(ShowMore)>
</div>
`;
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {Post, PostType} from '@mattermost/types/posts';
@@ -11,10 +10,36 @@ import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import PostMessageView from 'components/post_view/post_message_view/post_message_view';
import {act, renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
jest.mock('components/properties_card_view/propertyValueRenderer/post_preview_property_renderer/post_preview_property_renderer', () => {
return jest.fn(() => <div data-testid='post-preview-property-renderer-mock'>{'PostPreviewPropertyRenderer Mock'}</div>);
});
jest.mock('components/post_markdown', () => {
return jest.fn((props: any) => <div data-testid='post-markdown'>{props.message}</div>);
});
jest.mock('plugins/pluggable', () => {
return jest.fn(() => <div data-testid='pluggable-mock'/>);
});
/** ShowMore uses scrollHeight > maxHeight; jsdom reports scrollHeight as 0, so overflow never triggers without this. */
function stubShowMoreOverflowLayout(run: () => void) {
const scrollSpy = jest.spyOn(Element.prototype, 'scrollHeight', 'get').mockReturnValue(100);
const raf = window.requestAnimationFrame;
window.requestAnimationFrame = (cb: FrameRequestCallback) => {
cb(0);
return 0;
};
try {
run();
} finally {
window.requestAnimationFrame = raf;
scrollSpy.mockRestore();
}
}
describe('components/post_view/PostAttachment', () => {
const post = {
id: 'post_id',
@@ -37,56 +62,109 @@ describe('components/post_view/PostAttachment', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<PostMessageView {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<PostMessageView {...baseProps}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, on Show More', () => {
const wrapper = shallow(<PostMessageView {...baseProps}/>);
test('should match snapshot, on Show More', async () => {
// ShowMore sets isOverflow when textContainer.scrollHeight > maxHeight.
// Default max height (600px) is never exceeded by the mocked short message; use a tiny maxHeight.
let container: HTMLElement | undefined;
stubShowMoreOverflowLayout(() => {
container = renderWithContext(
<PostMessageView
{...baseProps}
maxHeight={1}
/>,
).container;
});
wrapper.setState({hasOverflow: true, collapse: true});
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(screen.getByRole('button', {name: /show more/i})).toBeInTheDocument();
});
expect(container).toBeDefined();
expect(container).toMatchSnapshot();
});
test('should match snapshot, on Show Less', () => {
const wrapper = shallow(<PostMessageView {...baseProps}/>);
test('should match snapshot, on Show Less', async () => {
let container: HTMLElement | undefined;
stubShowMoreOverflowLayout(() => {
container = renderWithContext(
<PostMessageView
{...baseProps}
maxHeight={1}
/>,
).container;
});
wrapper.setState({hasOverflow: true, collapse: false});
expect(wrapper).toMatchSnapshot();
await waitFor(() => {
expect(screen.getByRole('button', {name: /show more/i})).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', {name: /show more/i}));
await waitFor(() => {
expect(screen.getByRole('button', {name: /show less/i})).toBeInTheDocument();
});
expect(container).toBeDefined();
expect(container).toMatchSnapshot();
});
test('should match snapshot, on deleted post', () => {
const props = {...baseProps, post: {...post, state: Posts.POST_DELETED as 'DELETED'}};
const wrapper = shallow(<PostMessageView {...props}/>);
const instance = wrapper.instance() as PostMessageView;
const {container} = renderWithContext(<PostMessageView {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(instance.renderDeletedPost()).toMatchSnapshot();
expect(container).toMatchSnapshot();
// Verify the deleted post message is rendered
expect(screen.getByText('(message deleted)')).toBeInTheDocument();
});
test('should match snapshot, on edited post', () => {
const props = {...baseProps, post: {...post, edit_at: 1}};
const wrapper = shallow(<PostMessageView {...props}/>);
const {container} = renderWithContext(<PostMessageView {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match snapshot, on ephemeral post', () => {
const props = {...baseProps, post: {...post, type: Posts.POST_TYPES.EPHEMERAL as PostType}};
const wrapper = shallow(<PostMessageView {...props}/>);
const {container} = renderWithContext(<PostMessageView {...props}/>);
expect(wrapper).toMatchSnapshot();
expect(container).toMatchSnapshot();
});
test('should match checkOverflow state on handleHeightReceived change', () => {
const wrapper = shallow(<PostMessageView {...baseProps}/>);
const instance = wrapper.instance() as PostMessageView;
// PostMarkdown is mocked, so we get imageProps from the mock calls.
// Import the mocked PostMarkdown to access its calls.
const PostMarkdown = jest.requireMock('components/post_markdown');
wrapper.setState({checkOverflow: 0});
instance.handleHeightReceived(1);
expect(wrapper.state('checkOverflow')).toEqual(1);
PostMarkdown.mockClear();
renderWithContext(<PostMessageView {...baseProps}/>);
instance.handleHeightReceived(0);
expect(wrapper.state('checkOverflow')).toEqual(1);
// PostMarkdown should have been called with imageProps
const postMarkdownCalls = PostMarkdown.mock.calls;
expect(postMarkdownCalls.length).toBeGreaterThan(0);
const imageProps = postMarkdownCalls[0][0].imageProps;
expect(imageProps).toBeDefined();
expect(imageProps.onImageLoaded).toBeDefined();
// Call onImageLoaded with height > 0 (should trigger checkPostOverflow)
const initialCallCount = postMarkdownCalls.length;
act(() => {
imageProps.onImageLoaded(1);
});
// Component should re-render (PostMarkdown called again with updated checkOverflow)
expect(PostMarkdown.mock.calls.length).toBeGreaterThan(initialCallCount);
// Call with height 0 (should NOT trigger checkPostOverflow)
const callCountAfterFirst = PostMarkdown.mock.calls.length;
act(() => {
imageProps.onImageLoaded(0);
});
// Should not cause additional re-render since height is 0
expect(PostMarkdown.mock.calls.length).toEqual(callCountAfterFirst);
});
});
@@ -1,14 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import {act} from '@testing-library/react';
import React from 'react';
import {Preferences} from 'mattermost-redux/constants';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import PostList from './post_list';
import PostView from './post_view';
jest.mock('./post_list', () => {
const MockPostList = jest.fn(() => <div data-testid='post-list-mock'/>);
return {__esModule: true, default: MockPostList};
});
describe('components/post_view/post_view', () => {
const baseProps = {
lastViewedAt: 12345678,
@@ -20,9 +27,10 @@ describe('components/post_view/post_view', () => {
};
jest.useFakeTimers();
let rafSpy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => setTimeout(cb, 16));
let rafSpy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => setTimeout(cb, 16) as unknown as number);
beforeEach(() => {
rafSpy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => setTimeout(cb, 16));
rafSpy = jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => setTimeout(cb, 16) as unknown as number);
(PostList as unknown as jest.Mock).mockClear();
});
afterEach(() => {
@@ -30,28 +38,61 @@ describe('components/post_view/post_view', () => {
});
test('should match snapshot for channel loading', () => {
const wrapper = shallow(<PostView {...{...baseProps, channelLoading: true}}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<PostView {...{...baseProps, channelLoading: true}}/>);
expect(container).toMatchSnapshot();
});
test('should match snapshot for loaderForChangeOfPostsChunk', () => {
const wrapper = shallow(<PostView {...baseProps}/>);
wrapper.setState({loaderForChangeOfPostsChunk: true});
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<PostView {...baseProps}/>);
// Get changeUnreadChunkTimeStamp from PostList's last call props
const postListCalls = (PostList as unknown as jest.Mock).mock.calls;
const lastCallProps = postListCalls[postListCalls.length - 1][0];
act(() => {
lastCallProps.changeUnreadChunkTimeStamp(Date.now());
});
// loaderForChangeOfPostsChunk is now true, should show loading
expect(container).toMatchSnapshot();
});
test('unreadChunkTimeStamp should be set for first load of channel', () => {
const wrapper = shallow(<PostView {...{...baseProps, isFirstLoad: true}}/>);
expect(wrapper.state('unreadChunkTimeStamp')).toEqual(baseProps.lastViewedAt);
renderWithContext(<PostView {...{...baseProps, isFirstLoad: true}}/>);
// Verify PostList received the correct unreadChunkTimeStamp prop
const postListCalls = (PostList as unknown as jest.Mock).mock.calls;
const lastCallProps = postListCalls[postListCalls.length - 1][0];
expect(lastCallProps.unreadChunkTimeStamp).toEqual(baseProps.lastViewedAt);
});
test('changeUnreadChunkTimeStamp', () => {
const wrapper = shallow(<PostView {...{...baseProps, isFirstLoad: true}}/>);
expect(wrapper.state('unreadChunkTimeStamp')).toEqual(baseProps.lastViewedAt);
wrapper.find(PostList).prop<(lastViewedAt?: string) => void>('changeUnreadChunkTimeStamp')('1234678');
expect(wrapper.state('unreadChunkTimeStamp')).toEqual('1234678');
expect(wrapper.state('loaderForChangeOfPostsChunk')).toEqual(true);
jest.runOnlyPendingTimers();
expect(wrapper.state('loaderForChangeOfPostsChunk')).toEqual(false);
renderWithContext(<PostView {...{...baseProps, isFirstLoad: true}}/>);
// Verify initial unreadChunkTimeStamp
let postListCalls = (PostList as unknown as jest.Mock).mock.calls;
let lastCallProps = postListCalls[postListCalls.length - 1][0];
expect(lastCallProps.unreadChunkTimeStamp).toEqual(baseProps.lastViewedAt);
// Call changeUnreadChunkTimeStamp
act(() => {
lastCallProps.changeUnreadChunkTimeStamp('1234678');
});
// loaderForChangeOfPostsChunk should be true now (loading screen shown)
expect(screen.queryByTestId('post-list-mock')).not.toBeInTheDocument();
// Run pending timers (rAF callback)
act(() => {
jest.runOnlyPendingTimers();
});
// loaderForChangeOfPostsChunk should be false now, PostList shown again
expect(screen.getByTestId('post-list-mock')).toBeInTheDocument();
// Verify PostList received updated unreadChunkTimeStamp
postListCalls = (PostList as unknown as jest.Mock).mock.calls;
lastCallProps = postListCalls[postListCalls.length - 1][0];
expect(lastCallProps.unreadChunkTimeStamp).toEqual('1234678');
});
});