mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
MM-68022 Bulk migration H18-28 Enzyme to RTL (#35753)
This commit is contained in:
+464
-391
@@ -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 > Web Server > 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>
|
||||
`;
|
||||
|
||||
+1470
-1001
File diff suppressed because it is too large
Load Diff
+72
-16
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+26
-33
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+56
-132
@@ -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>
|
||||
`;
|
||||
|
||||
+75
-65
@@ -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>
|
||||
`;
|
||||
|
||||
+8
-10
@@ -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>
|
||||
`;
|
||||
|
||||
+13
-9
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+27
-41
@@ -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'});
|
||||
});
|
||||
});
|
||||
|
||||
+8
-6
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+378
-308
@@ -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>
|
||||
`;
|
||||
|
||||
+50
-35
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+278
-561
@@ -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>
|
||||
`;
|
||||
|
||||
+90
-133
@@ -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>
|
||||
`;
|
||||
|
||||
+117
-64
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+62
-30
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+940
-1301
File diff suppressed because it is too large
Load Diff
+1168
-1489
File diff suppressed because it is too large
Load Diff
+400
-570
File diff suppressed because it is too large
Load Diff
+243
-309
@@ -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>
|
||||
`;
|
||||
|
||||
+224
-324
@@ -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>
|
||||
|
||||
+57
-82
@@ -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>
|
||||
|
||||
+112
-159
@@ -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);
|
||||
|
||||
+301
-26
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+189
-43
@@ -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>
|
||||
`;
|
||||
|
||||
+12
-13
@@ -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];
|
||||
|
||||
+260
-41
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+273
-44
@@ -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>
|
||||
`;
|
||||
|
||||
+40
-3
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+242
-323
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+380
-449
@@ -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>
|
||||
<==secret==>
|
||||
</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>
|
||||
<==secret==>
|
||||
</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>
|
||||
|
||||
+38
-16
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+754
-114
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+639
-241
@@ -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>
|
||||
`;
|
||||
|
||||
+68
-24
@@ -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`);
|
||||
});
|
||||
});
|
||||
|
||||
+567
-241
@@ -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>
|
||||
`;
|
||||
|
||||
+77
-66
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+588
-217
@@ -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>
|
||||
`;
|
||||
|
||||
+132
-44
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+346
-506
@@ -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>
|
||||
|
||||
+85
-56
@@ -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);
|
||||
});
|
||||
|
||||
+603
-152
@@ -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>
|
||||
`;
|
||||
|
||||
+102
-45
@@ -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', () => {
|
||||
|
||||
+316
-286
@@ -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>
|
||||
`;
|
||||
|
||||
+72
-21
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+75
-122
@@ -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>
|
||||
|
||||
+26
-10
@@ -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);
|
||||
});
|
||||
|
||||
+59
-33
@@ -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>
|
||||
`;
|
||||
|
||||
+422
-551
File diff suppressed because it is too large
Load Diff
+189
-128
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+161
-172
@@ -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>
|
||||
`;
|
||||
|
||||
+104
-26
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user