Merge branch 'MM-50966-in-product-expansion' of github.com:mattermost/mattermost-server into MM-50966-in-product-expansion

Этот коммит содержится в:
Conor Macpherson
2023-03-31 13:39:44 -04:00
родитель 6fe45fe890 545d35e9cf
Коммит d74ff61fc8
301 изменённых файлов: 10713 добавлений и 12417 удалений

4
.github/workflows/channels-ci.yml поставляемый
Просмотреть файл

@@ -7,7 +7,7 @@ on:
- mono-repo*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ !contains( github.ref , 'heads/ref/master') }}
defaults:
run:
shell: bash
@@ -150,7 +150,7 @@ jobs:
env:
NODE_OPTIONS: --max_old_space_size=5120
run: |
# npm run test-ci --workspace=boards
npm run test-ci --workspace=boards
npm run test-ci --workspace=channels
npm run test-ci --workspace=platform/client
npm run test-ci --workspace=playbooks

2
.github/workflows/ci.yml поставляемый
Просмотреть файл

@@ -11,7 +11,7 @@ env:
go-version: "1.19.5"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ !contains( github.ref , 'heads/ref/master') }}
jobs:
check-mocks:
name: Check mocks

26
.github/workflows/codeql-analysis.yml поставляемый
Просмотреть файл

@@ -2,7 +2,7 @@ name: "CodeQL"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ !contains( github.ref , 'heads/ref/master') }}
on:
pull_request:
@@ -10,7 +10,7 @@ on:
branches: [ master ]
schedule:
- cron: '30 5,17 * * *'
permissions:
contents: read
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
language: [ 'go', 'javascript' ]
steps:
- name: Checkout repository
@@ -36,14 +36,26 @@ jobs:
with:
languages: ${{ matrix.language }}
debug: false
config-file: ./.github/codeql/codeql-config.yml
- name: Build
config-file: ./.github/codeql/codeql-config.yml
- name: Build JavaScript
uses: github/codeql-action/autobuild@v2
if: ${{ matrix.language == 'javascript' }}
- name: Setup go
uses: actions/setup-go@v2
with:
go-version: '1.20'
if: ${{ matrix.language == 'go' }}
- name: Build Golang
run: |
cd server
make setup-go-work
make build-linux-amd64
if: ${{ matrix.language == 'go' }}
# Perform Analysis
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v2

2
.github/workflows/e2e-tests-ci.yml поставляемый
Просмотреть файл

@@ -7,7 +7,7 @@ on:
- mono-repo*
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ !contains( github.ref , 'heads/ref/master') }}
defaults:
run:
shell: bash

4
.github/workflows/scorecards-analysis.yml поставляемый
Просмотреть файл

@@ -5,6 +5,10 @@ on:
schedule:
- cron: '44 6 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ !contains( github.ref , 'heads/ref/master') }}
# Declare default permissions as read only.
permissions: read-all

1
.gitignore поставляемый
Просмотреть файл

@@ -134,6 +134,7 @@ cprofile.out
*.test
webapp/coverage
/report.xml
junit.xml
.agignore
.ctags

Просмотреть файл

@@ -1 +1,9 @@
/plugin/ @mattermost/toolkit
/.github/workflows/channels-ci.yml @mattermost/web-platform
/webapp/package.json @mattermost/web-platform
/webapp/channels/package.json @mattermost/web-platform
/webapp/Makefile @mattermost/web-platform
/webapp/package-lock.json @mattermost/web-platform
/webapp/platform/*/package.json @mattermost/web-platform
/webapp/scripts @mattermost/web-platform

Просмотреть файл

@@ -47,6 +47,57 @@ describe('Create and delete board / card', () => {
should('have.value', 'Testing');
});
it('MM-T4275 Set up Board description', () => {
cy.visit('/boards');
// # Create an empty board and change tile to Testing
cy.findByText('Create an empty board').should('exist').click({force: true});
cy.get('.BoardComponent').should('exist');
// # Change Title
cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC);
// * Assert that the title is changed to "testing"
cy.findByPlaceholderText('Untitled board').
clear().
type('Testing').
type('{enter}').
should('have.value', 'Testing');
// # "Add icon" and "Show description" options appear
cy.findByText('Add icon').should('exist').click({force: true});
cy.findByText('show description').should('exist').click({force: true});
// # Click on "Add a description" below the board title and type "for testing purposes only"
cy.findByText('Add a description...').should('be.visible').wait(timeouts.HALF_SEC);
// * Assert that the editable description should be visible
cy.findByText('Add a description...').should('be.visible');
cy.findByText('Add a description...').click({force: true});
cy.get('.description').
click().
get('.description .MarkdownEditorInput').
type('for testing purposes only');
// # Click to other element to give some time for the description to be saved.
cy.findByPlaceholderText('Untitled board').click();
// * Assert that the description is changed to "for testing purposes only"
cy.findByText('for testing purposes only').should('be.visible');
// # Hide Description options should appear and click on it to hide description
cy.findByText('hide description').should('exist').click({force: true});
// * Assert that description should not appear"
cy.get('.description').should('not.exist');
// # Show Description options should appear and click on it to show description
cy.findByText('show description').should('exist').click({force: true});
// * Assert that the description "for testing purposes should be visible"
cy.findByText('for testing purposes only').should('be.visible');
});
it('MM-T5397 Can create and delete a board and a card', () => {
// Visit a page and create new empty board
cy.visit('/boards');

Просмотреть файл

@@ -334,7 +334,7 @@ describe('Pricing modal', () => {
// * Check that enterprise card action button shows Try free for 30 days
cy.get('#pricingModal').should('be.visible');
cy.get('#start_cloud_trial_btn').contains('Try free for 30 days');
cy.get('#start_cloud_trial_btn').contains('Start trial');
});
it('should open pricing modal when Upgrade button clicked while in enterprise trial sku', () => {
@@ -365,7 +365,7 @@ describe('Pricing modal', () => {
// * Check that enterprise card action button is disabled
cy.get('#enterprise > .bottom > .bottom_container').should('be.visible');
cy.get('#start_cloud_trial_btn').contains('Try free for 30 days');
cy.get('#start_cloud_trial_btn').contains('Start trial');
cy.get('#enterprise > .bottom > .bottom_container').should('be.visible');
cy.get('#start_cloud_trial_btn').should('be.disabled');
});

Просмотреть файл

@@ -102,7 +102,7 @@ describe('Self hosted Pricing modal', () => {
// * Check that enteprise trial button is available
cy.get('#pricingModal').should('be.visible');
cy.get('#enterprise').should('be.visible');
cy.get('#start_trial_btn').should('not.be.disabled').contains('Try free for 30 days');
cy.get('#start_trial_btn').should('not.be.disabled').contains('Start trial');
});
it('Upgrade button should open pricing modal admin users when the server has requested a trial before on free plan', () => {

Просмотреть файл

@@ -53,7 +53,7 @@ describe('Feature discovery cloud', () => {
const testForTrialButton = () => {
cy.get('#start_cloud_trial_btn').should('exist');
cy.get('#start_cloud_trial_btn').contains('Try free for 30 days');
cy.get('#start_cloud_trial_btn').contains('Start trial');
};
const testForUpgradeToProfessionalOption = () => {

Просмотреть файл

@@ -693,7 +693,7 @@ const defaultServerConfig: AdminConfig = {
WysiwygEditor: false,
PeopleProduct: false,
ReduceOnBoardingTaskList: false,
OnboardingAutoShowLinkedBoard: true,
OnboardingAutoShowLinkedBoard: false,
ThreadsEverywhere: false,
GlobalDrafts: true,
OnboardingTourTips: true,

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {expect, test} from '@e2e-support/test_fixture';
import {duration, isSmallScreen, wait} from '@e2e-support/util';
import {isSmallScreen} from '@e2e-support/util';
test('Intro to channel as regular user', async ({pw, pages, browserName, viewport}, testInfo) => {
// Create and sign in a new user
@@ -17,10 +17,10 @@ test('Intro to channel as regular user', async ({pw, pages, browserName, viewpor
await channelsPage.toBeVisible();
// Wait for Boards' bot image to be loaded
await pw.shouldHaveFeatureFlag('OnboardingAutoShowLinkedBoard', true);
const boardsWelcomePost = await channelsPage.getFirstPost();
await expect(await boardsWelcomePost.getProfileImage('boards')).toBeVisible();
await wait(duration.one_sec);
// await pw.shouldHaveFeatureFlag('OnboardingAutoShowLinkedBoard', true);
// const boardsWelcomePost = await channelsPage.getFirstPost();
// await expect(await boardsWelcomePost.getProfileImage('boards')).toBeVisible();
// await wait(duration.one_sec);
// Wait for Playbooks icon to be loaded in App bar, except in iphone
if (!isSmallScreen(viewport)) {

Просмотреть файл

@@ -7812,7 +7812,23 @@ func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezon
return ch, BuildResponse(r), nil
}
func (c *Client4) RequestTrialLicenseWithExtraFields(trialRequest *TrialLicenseRequest) (*Response, error) {
b, err := json.Marshal(trialRequest)
if err != nil {
return nil, NewAppError("RequestTrialLicense", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost("/trial-license", string(b))
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// RequestTrialLicense will request a trial license and install it in the server
// DEPRECATED - USE RequestTrialLicenseWithExtraFields (this method remains for backwards compatibility)
func (c *Client4) RequestTrialLicense(users int) (*Response, error) {
b, err := json.Marshal(map[string]any{"users": users, "terms_accepted": true})
if err != nil {

Просмотреть файл

@@ -51,9 +51,6 @@ type FeatureFlags struct {
CommandPalette bool
// Enable Boards as a product (multi-product architecture)
BoardsProduct bool
// A/B Test on posting a welcome message
SendWelcomePost bool
@@ -95,7 +92,6 @@ func (f *FeatureFlags) SetDefaults() {
f.InsightsEnabled = true
f.CommandPalette = false
f.CallsEnabled = true
f.BoardsProduct = false
f.SendWelcomePost = true
f.PostPriority = true
f.PeopleProduct = false
@@ -104,7 +100,7 @@ func (f *FeatureFlags) SetDefaults() {
f.ThreadsEverywhere = false
f.GlobalDrafts = true
f.WysiwygEditor = false
f.OnboardingAutoShowLinkedBoard = true
f.OnboardingAutoShowLinkedBoard = false
f.OnboardingTourTips = true
}

Просмотреть файл

@@ -84,6 +84,48 @@ type TrialLicenseRequest struct {
Users int `json:"users"`
TermsAccepted bool `json:"terms_accepted"`
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
ContactName string `json:"contact_name"`
ContactEmail string `json:"contact_email"`
CompanyName string `json:"company_name"`
CompanyCountry string `json:"company_country"`
CompanySize string `json:"company_size"`
}
// If any of the below fields are set, this is not a legacy request, and all fields should be validated
func (tlr *TrialLicenseRequest) IsLegacy() bool {
return tlr.CompanyCountry == "" && tlr.CompanyName == "" && tlr.CompanySize == "" && tlr.ContactName == ""
}
func (tlr *TrialLicenseRequest) IsValid() bool {
if !tlr.TermsAccepted {
return false
}
if tlr.Email == "" {
return false
}
if tlr.Users <= 0 {
return false
}
if tlr.CompanyCountry == "" {
return false
}
if tlr.CompanyName == "" {
return false
}
if tlr.CompanySize == "" {
return false
}
if tlr.ContactName == "" {
return false
}
return true
}
type Features struct {

Просмотреть файл

@@ -200,6 +200,100 @@ func TestLicenseRecordPreSave(t *testing.T) {
assert.NotZero(t, lr.CreateAt)
}
func TestIsLegacyTrialRequest(t *testing.T) {
legacyTr := &TrialLicenseRequest{
Email: "test@mattermost.com",
TermsAccepted: true,
SiteURL: "https://mattermost.com",
SiteName: "Mattermost",
Users: 100,
}
t.Run("legacy trial request", func(t *testing.T) {
assert.True(t, legacyTr.IsLegacy())
})
t.Run("legacy trial request with any non-legacy field set is not a legacy request", func(t *testing.T) {
legacyTr.CompanyCountry = "US"
assert.False(t, legacyTr.IsLegacy())
legacyTr.CompanyCountry = ""
legacyTr.CompanyName = "test company"
assert.False(t, legacyTr.IsLegacy())
legacyTr.CompanyName = ""
legacyTr.CompanySize = "50-100"
assert.False(t, legacyTr.IsLegacy())
legacyTr.CompanySize = ""
legacyTr.ContactName = "test user"
assert.False(t, legacyTr.IsLegacy())
legacyTr.ContactName = ""
assert.True(t, legacyTr.IsLegacy())
})
}
func TestTrialLicenseRequestIsValid(t *testing.T) {
validTlr := &TrialLicenseRequest{
Email: "test@test.com",
Users: 100,
CompanyCountry: "US",
CompanyName: "Test Company",
CompanySize: "50-100",
ContactName: "Test User",
TermsAccepted: true,
}
resetBaseRequest := func() {
validTlr = &TrialLicenseRequest{
Email: "test@test.com",
Users: 100,
CompanyCountry: "US",
CompanyName: "Test Company",
CompanySize: "50-100",
ContactName: "Test User",
TermsAccepted: true,
}
}
t.Run("valid request", func(t *testing.T) {
resetBaseRequest()
assert.Equal(t, true, validTlr.IsValid())
})
t.Run("no terms", func(t *testing.T) {
resetBaseRequest()
validTlr.TermsAccepted = false
assert.Equal(t, false, validTlr.IsValid())
})
t.Run("no email", func(t *testing.T) {
resetBaseRequest()
validTlr.Email = ""
assert.Equal(t, false, validTlr.IsValid())
})
t.Run("no CompanyCountry", func(t *testing.T) {
resetBaseRequest()
validTlr.CompanyCountry = ""
assert.Equal(t, false, validTlr.IsValid())
})
t.Run("no CompanyName", func(t *testing.T) {
resetBaseRequest()
validTlr.CompanyName = ""
assert.Equal(t, false, validTlr.IsValid())
})
t.Run("no CompanySize", func(t *testing.T) {
resetBaseRequest()
validTlr.CompanySize = ""
assert.Equal(t, false, validTlr.IsValid())
})
t.Run("Bad User Count", func(t *testing.T) {
resetBaseRequest()
validTlr.Users = 0
assert.Equal(t, false, validTlr.IsValid())
})
}
func TestLicense_IsTrialLicense(t *testing.T) {
t.Run("detect trial license directly from the flag", func(t *testing.T) {
license := &License{

Просмотреть файл

@@ -118,9 +118,6 @@ var BuildDate string
var BuildHash string
var BuildHashEnterprise string
var BuildEnterpriseReady string
var BuildHashBoards string
var BuildBoards string
var BuildHashPlaybooks string
var versionsWithoutHotFixes []string
func init() {

Просмотреть файл

@@ -86,15 +86,6 @@ else
BUILD_CLIENT = false
endif
# Boards
BUILD_BOARDS = true
BUILD_HASH_BOARDS = $(BUILD_HASH)
export MM_FEATUREFLAGS_BoardsProduct=true
# Playbooks
BUILD_PLAYBOOKS ?= true
BUILD_HASH_PLAYBOOKS = $(BUILD_HASH)
# We need current user's UID for `run-haserver` so docker compose does not run server
# as root and mess up file permissions for devs. When running like this HOME will be blank
# and docker will add '/', so we need to set the go-build cache location or we'll get
@@ -116,9 +107,6 @@ LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildDate=$(BUIL
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHash=$(BUILD_HASH)"
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)"
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)"
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHashBoards=$(BUILD_HASH_BOARDS)"
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildBoards=$(BUILD_BOARDS)"
LDFLAGS += -X "github.com/mattermost/mattermost-server/v6/model.BuildHashPlaybooks=$(BUILD_HASH_PLAYBOOKS)"
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
@@ -462,9 +450,9 @@ else
endif
test-server-race: test-server-pre
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=false ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=true ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(BOARDS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
MM_DISABLE_PLAYBOOKS=false MM_FEATUREFLAGS_BoardsProduct=false ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=true ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=false ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(BOARDS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
MM_DISABLE_PLAYBOOKS=false MM_DISABLE_BOARDS=true ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" "atomic"
ifneq ($(IS_CI),true)
ifneq ($(MM_NO_DOCKER),true)
ifneq ($(TEMP_DOCKER_SERVICES),)
@@ -475,9 +463,9 @@ ifneq ($(IS_CI),true)
endif
test-server: test-server-pre
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=false ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=true ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(BOARDS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
MM_DISABLE_PLAYBOOKS=false MM_FEATUREFLAGS_BoardsProduct=false ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=true ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=false ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(BOARDS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
MM_DISABLE_PLAYBOOKS=false MM_DISABLE_BOARDS=true ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "45m" "count"
ifneq ($(IS_CI),true)
ifneq ($(MM_NO_DOCKER),true)
ifneq ($(TEMP_DOCKER_SERVICES),)
@@ -489,19 +477,19 @@ endif
test-server-ee: check-prereqs-enterprise start-docker go-junit-report do-cover-file ## Runs EE tests.
@echo Running only EE tests
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=false ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "20m" "count"
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=true ./scripts/test.sh "$(GO)" "$(GOFLAGS)" "$(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "20m" "count"
test-server-quick: check-prereqs-enterprise ## Runs only quick tests.
ifeq ($(BUILD_ENTERPRISE_READY),true)
@echo Running all tests
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=false $(GO) test $(GOFLAGS) -short $(TE_PACKAGES) $(EE_PACKAGES)
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=true $(GO) test $(GOFLAGS) -short $(BOARDS_PACKAGES)
MM_DISABLE_PLAYBOOKS=false MM_FEATUREFLAGS_BoardsProduct=false $(GO) test $(GOFLAGS) -short $(PLAYBOOKS_PACKAGES)
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=true $(GO) test $(GOFLAGS) -short $(TE_PACKAGES) $(EE_PACKAGES)
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=false $(GO) test $(GOFLAGS) -short $(BOARDS_PACKAGES)
MM_DISABLE_PLAYBOOKS=false MM_DISABLE_BOARDS=true $(GO) test $(GOFLAGS) -short $(PLAYBOOKS_PACKAGES)
else
@echo Running only TE tests
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=false $(GO) test $(GOFLAGS) -short $(TE_PACKAGES)
MM_DISABLE_PLAYBOOKS=true MM_FEATUREFLAGS_BoardsProduct=true $(GO) test $(GOFLAGS) -short $(BOARDS_PACKAGES)
MM_DISABLE_PLAYBOOKS=false MM_FEATUREFLAGS_BoardsProduct=false $(GO) test $(GOFLAGS) -short $(PLAYBOOKS_PACKAGES)
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=true $(GO) test $(GOFLAGS) -short $(TE_PACKAGES)
MM_DISABLE_PLAYBOOKS=true MM_DISABLE_BOARDS=false $(GO) test $(GOFLAGS) -short $(BOARDS_PACKAGES)
MM_DISABLE_PLAYBOOKS=false MM_DISABLE_BOARDS=true $(GO) test $(GOFLAGS) -short $(PLAYBOOKS_PACKAGES)
endif
internal-test-web-client: ## Runs web client tests.

Просмотреть файл

@@ -222,11 +222,6 @@ func populateServices(boardsProd *boardsProduct, services map[product.ServiceKey
}
func (bp *boardsProduct) Start() error {
if !bp.configService.Config().FeatureFlags.BoardsProduct {
bp.logger.Info("Boards product disabled via feature flag")
return nil
}
bp.logger.Info("Starting boards service")
adapter := newServiceAPIAdapter(bp)

Просмотреть файл

@@ -80,12 +80,8 @@ func CreateBoardsConfig(mmconfig mm_model.Config, baseURL string, serverID strin
showFullName = *mmconfig.PrivacySettings.ShowFullName
}
serverRoot := baseURL + "/plugins/focalboard"
if mmconfig.FeatureFlags.BoardsProduct {
serverRoot = baseURL + "/boards"
}
return &config.Configuration{
ServerRoot: serverRoot,
ServerRoot: baseURL + "/boards",
Port: -1,
DBType: *mmconfig.SqlSettings.DriverName,
DBConfigString: *mmconfig.SqlSettings.DataSource,

Просмотреть файл

@@ -60,7 +60,21 @@ func (api *API) InitCloud() {
api.BaseRoutes.Cloud.Handle("/delete-workspace", api.APISessionRequired(selfServeDeleteWorkspace)).Methods(http.MethodDelete)
}
func ensureCloudInterface(c *Context, where string) bool {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError(where, "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
return false
}
return true
}
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getSubscription")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -102,6 +116,10 @@ func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
}
func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.changeSubscription")
if !ensured {
return
}
userId := c.AppContext.Session().UserId
if !c.App.Channels().License().IsCloud() {
@@ -176,6 +194,11 @@ func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
}
func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.requestCloudTrial")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -218,13 +241,8 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
}
func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) {
c.SetPermissionError(model.PermissionSysconsoleWriteBilling)
ensured := ensureCloudInterface(c, "Api4.validateBusinessEmail")
if !ensured {
return
}
@@ -263,6 +281,11 @@ func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
}
func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.validateWorkspaceBusinessEmail")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -309,6 +332,11 @@ func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.R
}
func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getSelfHostedProducts")
if !ensured {
return
}
products, err := c.App.Cloud().GetSelfHostedProducts(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("Api4.getSelfHostedProducts", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
@@ -343,6 +371,11 @@ func getSelfHostedProducts(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getCloudProducts")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -384,6 +417,11 @@ func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getCloudLimits")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudLimits", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -405,6 +443,11 @@ func getCloudLimits(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getCloudCustomer")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -432,6 +475,11 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getLicenseSelfServeStatus")
if !ensured {
return
}
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageLicenseInformation) {
c.SetPermissionError(model.PermissionManageLicenseInformation)
return
@@ -460,6 +508,11 @@ func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Reques
}
func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.updateCloudCustomer")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomer", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -498,6 +551,11 @@ func updateCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
}
func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.updateCloudCustomerAddress")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.updateCloudCustomerAddress", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -536,6 +594,11 @@ func updateCloudCustomerAddress(c *Context, w http.ResponseWriter, r *http.Reque
}
func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.createCustomerPayment")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.createCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -567,6 +630,11 @@ func createCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
}
func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.confirmCustomerPayment")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.confirmCustomerPayment", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -604,6 +672,11 @@ func confirmCustomerPayment(c *Context, w http.ResponseWriter, r *http.Request)
}
func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getInvoicesForSubscription")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -630,6 +703,11 @@ func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Reque
}
func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.getSubscriptionInvoicePDF")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -665,6 +743,11 @@ func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Reques
}
func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.handleCWSWebhook")
if !ensured {
return
}
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.license_error", nil, "", http.StatusForbidden)
return
@@ -765,12 +848,12 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
}
func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
ensured := ensureCloudInterface(c, "Api4.handleCheckCWSConnection")
if !ensured {
return
}
if err := cloud.CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
if err := c.App.Cloud().CheckCWSConnection(c.AppContext.Session().UserId); err != nil {
c.Err = model.NewAppError("Api4.handleCWSHealthCheck", "api.server.cws.health_check.app_error", nil, "CWS Server is not available.", http.StatusInternalServerError)
return
}
@@ -779,6 +862,11 @@ func handleCheckCWSConnection(c *Context, w http.ResponseWriter, r *http.Request
}
func selfServeDeleteWorkspace(c *Context, w http.ResponseWriter, r *http.Request) {
ensured := ensureCloudInterface(c, "Api4.selfServeDeleteWorkspace")
if !ensured {
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.selfServeDeleteWorkspace", "api.cloud.app_error", nil, err.Error(), http.StatusBadRequest)

Просмотреть файл

@@ -20,6 +20,15 @@ func Test_getCloudLimits(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.App.Srv().RemoveLicense()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
@@ -34,6 +43,15 @@ func Test_getCloudLimits(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
cloud := &mocks.CloudInterface{}
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, errors.New("Unable to get limits"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = cloud
th.App.Srv().SetLicense(model.NewTestLicense())
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
@@ -310,30 +328,6 @@ func Test_requestTrial(t *testing.T) {
}
func Test_validateBusinessEmail(t *testing.T) {
t.Run("Returns forbidden for non admin executors", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
invalidEmail := model.ValidateBusinessEmailRequest{Email: "invalid@gmail.com"}
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, invalidEmail.Email).Return(errors.New("invalid email"))
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
res, err := th.Client.ValidateBusinessEmail(&invalidEmail)
require.Error(t, err)
require.Equal(t, http.StatusForbidden, res.StatusCode, "403")
})
t.Run("Returns forbidden for invalid business email", func(t *testing.T) {
th := Setup(t).InitBasic()

Просмотреть файл

@@ -39,9 +39,8 @@ func (api *API) InitHostedCustomer() {
}
func ensureSelfHostedAdmin(c *Context, where string) {
cloud := c.App.Cloud()
if cloud == nil {
c.Err = model.NewAppError(where, "api.server.cws.needs_enterprise_edition", nil, "", http.StatusBadRequest)
ensured := ensureCloudInterface(c, where)
if !ensured {
return
}

Просмотреть файл

@@ -210,11 +210,7 @@ func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
var trialRequest struct {
Users int `json:"users"`
TermsAccepted bool `json:"terms_accepted"`
ReceiveEmailsAccepted bool `json:"receive_emails_accepted"`
}
var trialRequest *model.TrialLicenseRequest
b, readErr := io.ReadAll(r.Body)
if readErr != nil {
@@ -223,8 +219,16 @@ func requestTrialLicense(c *Context, w http.ResponseWriter, r *http.Request) {
}
json.Unmarshal(b, &trialRequest)
if err := c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted); err != nil {
c.Err = err
var appErr *model.AppError
// If any of the newly supported trial request fields are set (ie, not a legacy request), process this as a new trial request (requiring the new fields) otherwise fall back on the old method.
if !trialRequest.IsLegacy() {
appErr = c.App.Channels().RequestTrialLicenseWithExtraFields(c.AppContext.Session().UserId, trialRequest)
} else {
appErr = c.App.Channels().RequestTrialLicense(c.AppContext.Session().UserId, trialRequest.Users, trialRequest.TermsAccepted, trialRequest.ReceiveEmailsAccepted)
}
if appErr != nil {
c.Err = appErr
return
}

Просмотреть файл

@@ -223,6 +223,154 @@ func TestRemoveLicenseFile(t *testing.T) {
})
}
func TestRequestTrialLicenseWithExtraFields(t *testing.T) {
th := Setup(t)
defer th.TearDown()
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil)
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
cloud := mocks.CloudInterface{}
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065/" })
nUsers := 1
validTrialRequest := &model.TrialLicenseRequest{
Email: "test@mattermost.com",
Users: nUsers,
TermsAccepted: true,
CompanyCountry: "US",
CompanyName: "mattermost",
CompanySize: "1-10",
ContactName: "Matter Most",
}
t.Run("permission denied", func(t *testing.T) {
resp, err := th.Client.RequestTrialLicenseWithExtraFields(&model.TrialLicenseRequest{})
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("trial license user count less than current users", func(t *testing.T) {
license := model.NewTestLicense()
license.Features.Users = model.NewInt(nUsers)
licenseJSON, jsonErr := json.Marshal(license)
require.NoError(t, jsonErr)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
response := map[string]string{
"license": string(licenseJSON),
}
err := json.NewEncoder(res).Encode(response)
require.NoError(t, err)
}))
defer testServer.Close()
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON))
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL })
defer func(requestTrialURL string) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL })
}(originalCwsUrl)
cloud.On("ValidateBusinessEmail", mock.Anything, mock.Anything).Return(nil)
resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest)
CheckErrorID(t, err, "api.license.add_license.unique_users.app_error")
CheckBadRequestStatus(t, resp)
})
t.Run("returns status 451 when it receives status 451", func(t *testing.T) {
license := model.NewTestLicense()
license.Features.Users = model.NewInt(nUsers)
licenseJSON, jsonErr := json.Marshal(license)
require.NoError(t, jsonErr)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusUnavailableForLegalReasons)
}))
defer testServer.Close()
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON))
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL })
defer func(requestTrialURL string) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL })
}(originalCwsUrl)
resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest)
require.Error(t, err)
require.Equal(t, resp.StatusCode, 451)
})
t.Run("returns status 400 if request is a mix of legacy and new fields", func(t *testing.T) {
validTrialRequest.CompanyCountry = ""
validTrialRequest.Users = 100
defer func() { validTrialRequest.CompanyCountry = "US" }()
license := model.NewTestLicense()
license.Features.Users = model.NewInt(nUsers)
licenseJSON, jsonErr := json.Marshal(license)
require.NoError(t, jsonErr)
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
response := map[string]string{
"license": string(licenseJSON),
}
err := json.NewEncoder(res).Encode(response)
require.NoError(t, err)
}))
defer testServer.Close()
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseJSON))
utils.LicenseValidator = &mockLicenseValidator
licenseManagerMock := &mocks.LicenseInterface{}
licenseManagerMock.On("CanStartTrial").Return(true, nil).Once()
th.App.Srv().Platform().SetLicenseManager(licenseManagerMock)
originalCwsUrl := *th.App.Srv().Config().CloudSettings.CWSURL
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = testServer.URL })
defer func(requestTrialURL string) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.CloudSettings.CWSURL = requestTrialURL })
}(originalCwsUrl)
cloud.On("ValidateBusinessEmail", mock.Anything, mock.Anything).Return(nil)
resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest)
CheckErrorID(t, err, "api.license.request-trial.bad-request")
CheckBadRequestStatus(t, resp)
})
th.App.Srv().Platform().SetLicenseManager(nil)
t.Run("trial license should fail if LicenseManager is nil", func(t *testing.T) {
resp, err := th.SystemAdminClient.RequestTrialLicenseWithExtraFields(validTrialRequest)
CheckErrorID(t, err, "api.license.upgrade_needed.app_error")
CheckForbiddenStatus(t, resp)
})
}
func TestRequestTrialLicense(t *testing.T) {
th := Setup(t)
defer th.TearDown()

Просмотреть файл

@@ -30,6 +30,7 @@ const ServerKey product.ServiceKey = "server"
type licenseSvc interface {
GetLicense() *model.License
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError
}
// Channels contains all channels related state.
@@ -323,6 +324,10 @@ func (ch *Channels) License() *model.License {
return ch.licenseSvc.GetLicense()
}
func (ch *Channels) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError {
return ch.licenseSvc.RequestTrialLicenseWithExtraFields(requesterID, trialRequest)
}
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
receiveEmailsAccepted)

Просмотреть файл

@@ -36,6 +36,51 @@ func (w *licenseWrapper) GetLicense() *model.License {
return w.srv.License()
}
func (w *licenseWrapper) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError {
if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
requester, err := w.srv.userService.GetUser(requesterID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
return model.NewAppError("RequestTrialLicense", MissingAccountError, nil, "", http.StatusNotFound).Wrap(err)
default:
return model.NewAppError("RequestTrialLicense", "app.user.get_by_username.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if w.srv.Cloud.ValidateBusinessEmail(requesterID, trialRequest.ContactEmail) != nil {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request.business-email", nil, "", http.StatusBadRequest)
}
// Create a new struct only using the fields from the request that are allowed to be set by the client
sanitizedRequest := &model.TrialLicenseRequest{
ServerID: w.srv.TelemetryId(),
Name: requester.GetDisplayName(model.ShowFullName),
Email: requester.Email,
SiteName: *w.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL,
Users: trialRequest.Users,
TermsAccepted: trialRequest.TermsAccepted,
ReceiveEmailsAccepted: trialRequest.ReceiveEmailsAccepted,
ContactName: trialRequest.ContactName,
ContactEmail: trialRequest.ContactEmail,
CompanyName: trialRequest.CompanyName,
CompanySize: trialRequest.CompanySize,
CompanyCountry: trialRequest.CompanyCountry,
}
if !sanitizedRequest.IsValid() {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
}
return w.srv.platform.RequestTrialLicense(sanitizedRequest)
}
// DEPRECATED - use RequestTrialLicenseWithExtraFields instead. This function remains to support the Plugin API.
func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)

Просмотреть файл

@@ -363,6 +363,12 @@ func (a *App) getImagesForPost(c request.CTX, post *model.Post, imageURLs []stri
}
for _, imageURL := range imageURLs {
// prevent infinite loop if a OG image URL is the same post's permalink
resolvedURL := resolveMetadataURL(imageURL, a.GetSiteURL())
if looksLikeAPermalink(resolvedURL, a.GetSiteURL()) {
continue
}
if _, image, _, err := a.getLinkMetadata(c, imageURL, post.CreateAt, isNewPost, post.GetPreviewedPostProp()); err != nil {
appErr, ok := err.(*model.AppError)
isNotFound := ok && appErr.StatusCode == http.StatusNotFound
@@ -651,6 +657,9 @@ func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64,
var res *http.Response
res, err = client.Do(request)
if err != nil {
mlog.Warn("error fetching OG image data", mlog.Err(err))
}
if res != nil {
body = res.Body

Просмотреть файл

@@ -18,6 +18,10 @@ import (
"testing"
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
"github.com/stretchr/testify/mock"
"github.com/dyatlov/go-opengraph/opengraph"
ogimage "github.com/dyatlov/go-opengraph/opengraph/types/image"
"github.com/stretchr/testify/assert"
@@ -1311,6 +1315,49 @@ func TestGetImagesForPost(t *testing.T) {
images := th.App.getImagesForPost(th.Context, post, []string{}, false)
assert.Equal(t, images, map[string]*model.PostImage{})
})
t.Run("should not process OpenGraph image that's a Mattermost permalink", func(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
ogURL := "https://example.com/index.html"
imageURL := th.App.GetSiteURL() + "/pl/qwertyuiopasdfghjklzxcvbnm"
post := &model.Post{
Id: "qwertyuiopasdfghjklzxcvbnm",
Metadata: &model.PostMetadata{
Embeds: []*model.PostEmbed{
{
Type: model.PostEmbedOpengraph,
URL: ogURL,
Data: &opengraph.OpenGraph{
Images: []*ogimage.Image{
{
URL: imageURL,
},
},
},
},
},
},
}
mockPostStore := mocks.PostStore{}
mockPostStore.On("GetSingle", "qwertyuiopasdfghjklzxcvbnm", false).RunFn = func(args mock.Arguments) {
assert.Fail(t, "should not have tried to process Mattermost permalink in OG image URL")
}
mockLinkMetadataStore := mocks.LinkMetadataStore{}
mockLinkMetadataStore.On("Get", mock.Anything, mock.Anything).Return(nil, store.NewErrNotFound("mock resource", "mock ID"))
mockStore := th.App.Srv().Store().(*mocks.Store)
mockStore.On("Post").Return(&mockPostStore)
mockStore.On("LinkMetadata").Return(&mockLinkMetadataStore)
images := th.App.getImagesForPost(th.Context, post, []string{}, false)
assert.Equal(t, 0, len(images))
assert.Equal(t, images, map[string]*model.PostImage{})
})
}
func TestGetEmojiNamesForString(t *testing.T) {

Просмотреть файл

@@ -72,14 +72,14 @@ func (s *Server) initializeProducts(
func (s *Server) shouldStart(product string) bool {
if product == "boards" {
if !s.Config().FeatureFlags.BoardsProduct {
s.Log().Warn("Skipping boards start: not enabled via feature flag")
if os.Getenv("MM_DISABLE_BOARDS") == "true" {
s.Log().Warn("Skipping Boards start: disabled via env var")
return false
}
}
if product == "playbooks" {
if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" {
s.Log().Warn("Skipping playbooks start: disabled via env var")
s.Log().Warn("Skipping Playbooks start: disabled via env var")
return false
}
}

Просмотреть файл

@@ -152,6 +152,11 @@ var searchUserStoreTests = []searchTest{
Fn: testSearchUsersInTeamUsernameWithUnderscore,
Tags: []string{EngineAll},
},
{
Name: "Should support search all users containing a substring in any name",
Fn: testSearchUserBySubstringInAnyName,
Tags: []string{EngineAll},
},
}
func TestSearchUserStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) {
@@ -865,6 +870,76 @@ func testSearchUsersByFullName(t *testing.T, th *SearchTestHelper) {
})
}
func testSearchUserBySubstringInAnyName(t *testing.T, th *SearchTestHelper) {
t.Run("Should search users by substring in first name", func(t *testing.T) {
userAlternate, err := th.createUser("user-alternate", "user-alternate", "alternate helloooo first name", "alternate")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
// searching user without specifying team
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search("", "hello", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
// adding user to team to search by team
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options = createDefaultOptions(true, false, false)
users, err = th.Store.User().Search(th.Team.Id, "hello", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
t.Run("Should search users by substring in last name name", func(t *testing.T) {
userAlternate, err := th.createUser("user-alternate", "user-alternate", "alternate", "alternate helloooo last name")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search("", "hello", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
// adding user to team to search by team
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options = createDefaultOptions(true, false, false)
users, err = th.Store.User().Search(th.Team.Id, "hello", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
t.Run("Should search users by substring in nickname name", func(t *testing.T) {
userAlternate, err := th.createUser("user-alternate", "alternate helloooo nickname", "alternate hello first name", "alternate")
require.NoError(t, err)
defer th.deleteUser(userAlternate)
options := createDefaultOptions(true, false, false)
users, err := th.Store.User().Search("", "hello", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
// adding user to team to search by team
err = th.addUserToTeams(userAlternate, []string{th.Team.Id})
require.NoError(t, err)
err = th.addUserToChannels(userAlternate, []string{th.ChannelBasic.Id})
require.NoError(t, err)
options = createDefaultOptions(true, false, false)
users, err = th.Store.User().Search(th.Team.Id, "hello", options)
require.NoError(t, err)
th.assertUsersMatchInAnyOrder(t, []*model.User{userAlternate}, users)
})
}
func createDefaultOptions(allowFullName, allowEmails, allowInactive bool) *model.UserSearchOptions {
return &model.UserSearchOptions{
AllowFullNames: allowFullName,

Просмотреть файл

@@ -22,6 +22,25 @@ func newSqlRemoteClusterStore(sqlStore *SqlStore) store.RemoteClusterStore {
return &sqlRemoteClusterStore{sqlStore}
}
func remoteClusterFields(prefix string) []string {
if prefix != "" && !strings.HasSuffix(prefix, ".") {
prefix = prefix + "."
}
return []string{
prefix + "RemoteId",
prefix + "RemoteTeamId",
prefix + "Name",
prefix + "DisplayName",
prefix + "SiteURL",
prefix + "CreateAt",
prefix + "LastPingAt",
prefix + "Token",
prefix + "RemoteToken",
prefix + "Topics",
prefix + "CreatorId",
}
}
func (s sqlRemoteClusterStore) Save(remoteCluster *model.RemoteCluster) (*model.RemoteCluster, error) {
remoteCluster.PreSave()
if err := remoteCluster.IsValid(); err != nil {
@@ -89,7 +108,7 @@ func (s sqlRemoteClusterStore) Delete(remoteId string) (bool, error) {
func (s sqlRemoteClusterStore) Get(remoteId string) (*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select("*").
Select(remoteClusterFields("")...).
From("RemoteClusters").
Where(sq.Eq{"RemoteId": remoteId})
@@ -107,7 +126,7 @@ func (s sqlRemoteClusterStore) Get(remoteId string) (*model.RemoteCluster, error
func (s sqlRemoteClusterStore) GetAll(filter model.RemoteClusterQueryFilter) ([]*model.RemoteCluster, error) {
query := s.getQueryBuilder().
Select("rc.*").
Select(remoteClusterFields("rc")...).
From("RemoteClusters rc")
if filter.InChannel != "" {

Просмотреть файл

@@ -6,6 +6,7 @@ package sqlstore
import (
"database/sql"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/channels/store"
@@ -71,12 +72,32 @@ func (s SqlSharedChannelStore) Save(sc *model.SharedChannel) (sh *model.SharedCh
return sc, nil
}
func sharedChannelFields(prefix string) []string {
if prefix != "" && !strings.HasSuffix(prefix, ".") {
prefix = prefix + "."
}
return []string{
prefix + "ChannelId",
prefix + "TeamId",
prefix + "Home",
prefix + "ReadOnly",
prefix + "ShareName",
prefix + "ShareDisplayName",
prefix + "SharePurpose",
prefix + "ShareHeader",
prefix + "CreatorId",
prefix + "CreateAt",
prefix + "UpdateAt",
prefix + "RemoteId",
}
}
// Get fetches a shared channel by channel_id.
func (s SqlSharedChannelStore) Get(channelId string) (*model.SharedChannel, error) {
var sc model.SharedChannel
query := s.getQueryBuilder().
Select("*").
Select(sharedChannelFields("")...).
From("SharedChannels").
Where(sq.Eq{"SharedChannels.ChannelId": channelId})
@@ -173,15 +194,15 @@ func (s SqlSharedChannelStore) GetAllCount(opts model.SharedChannelFilterOpts) (
}
func (s SqlSharedChannelStore) getSharedChannelsQuery(opts model.SharedChannelFilterOpts, forCount bool) sq.SelectBuilder {
var selectStr string
var selectFields []string
if forCount {
selectStr = "count(sc.ChannelId)"
selectFields = []string{"count(sc.ChannelId)"}
} else {
selectStr = "sc.*"
selectFields = sharedChannelFields("sc")
}
query := s.getQueryBuilder().
Select(selectStr).
Select(selectFields...).
From("SharedChannels AS sc")
if opts.MemberId != "" {
@@ -364,12 +385,30 @@ func (s SqlSharedChannelStore) UpdateRemote(remote *model.SharedChannelRemote) (
return remote, nil
}
func sharedChannelRemoteFields(prefix string) []string {
if prefix != "" && !strings.HasSuffix(prefix, ".") {
prefix = prefix + "."
}
return []string{
prefix + "Id",
prefix + "ChannelId",
prefix + "CreatorId",
prefix + "CreateAt",
prefix + "UpdateAt",
prefix + "IsInviteAccepted",
prefix + "IsInviteConfirmed",
prefix + "RemoteId",
prefix + "LastPostUpdateAt",
prefix + "LastPostId",
}
}
// GetRemote fetches a shared channel remote by id.
func (s SqlSharedChannelStore) GetRemote(id string) (*model.SharedChannelRemote, error) {
var remote model.SharedChannelRemote
query := s.getQueryBuilder().
Select("*").
Select(sharedChannelRemoteFields("")...).
From("SharedChannelRemotes").
Where(sq.Eq{"SharedChannelRemotes.Id": id})
@@ -392,7 +431,7 @@ func (s SqlSharedChannelStore) GetRemoteByIds(channelId string, remoteId string)
var remote model.SharedChannelRemote
query := s.getQueryBuilder().
Select("*").
Select(sharedChannelRemoteFields("")...).
From("SharedChannelRemotes").
Where(sq.Eq{"SharedChannelRemotes.ChannelId": channelId}).
Where(sq.Eq{"SharedChannelRemotes.RemoteId": remoteId})
@@ -416,7 +455,7 @@ func (s SqlSharedChannelStore) GetRemotes(opts model.SharedChannelRemoteFilterOp
remotes := []*model.SharedChannelRemote{}
query := s.getQueryBuilder().
Select("*").
Select(sharedChannelRemoteFields("")...).
From("SharedChannelRemotes")
if opts.ChannelId != "" {
@@ -570,6 +609,20 @@ func (s SqlSharedChannelStore) GetRemotesStatus(channelId string) ([]*model.Shar
return status, nil
}
func sharedChannelUserFields(prefix string) []string {
if prefix != "" && !strings.HasSuffix(prefix, ".") {
prefix = prefix + "."
}
return []string{
prefix + "Id",
prefix + "UserId",
prefix + "ChannelId",
prefix + "RemoteId",
prefix + "CreateAt",
prefix + "LastSyncAt",
}
}
// SaveUser inserts a new shared channel user record to the SharedChannelUsers table.
func (s SqlSharedChannelStore) SaveUser(scUser *model.SharedChannelUser) (*model.SharedChannelUser, error) {
scUser.PreSave()
@@ -578,7 +631,7 @@ func (s SqlSharedChannelStore) SaveUser(scUser *model.SharedChannelUser) (*model
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelUsers").
Columns("Id", "UserId", "ChannelId", "RemoteId", "CreateAt", "LastSyncAt").
Columns(sharedChannelUserFields("")...).
Values(scUser.Id, scUser.UserId, scUser.ChannelId, scUser.RemoteId, scUser.CreateAt, scUser.LastSyncAt).
ToSql()
if err != nil {
@@ -595,7 +648,7 @@ func (s SqlSharedChannelStore) GetSingleUser(userID string, channelID string, re
var scu model.SharedChannelUser
squery, args, err := s.getQueryBuilder().
Select("*").
Select(sharedChannelUserFields("")...).
From("SharedChannelUsers").
Where(sq.Eq{"SharedChannelUsers.UserId": userID}).
Where(sq.Eq{"SharedChannelUsers.RemoteId": remoteID}).
@@ -618,7 +671,7 @@ func (s SqlSharedChannelStore) GetSingleUser(userID string, channelID string, re
// GetUsersForUser fetches all shared channel user records based on userID.
func (s SqlSharedChannelStore) GetUsersForUser(userID string) ([]*model.SharedChannelUser, error) {
squery, args, err := s.getQueryBuilder().
Select("*").
Select(sharedChannelUserFields("")...).
From("SharedChannelUsers").
Where(sq.Eq{"SharedChannelUsers.UserId": userID}).
ToSql()
@@ -645,7 +698,9 @@ func (s SqlSharedChannelStore) GetUsersForSync(filter model.GetUsersForSyncFilte
}
query := s.getQueryBuilder().
Select("u.*").
Select("u.Id", "u.CreateAt", "u.UpdateAt", "u.DeleteAt", "u.Username", "u.Email", "u.EmailVerified",
"u.Nickname", "u.FirstName", "u.LastName", "u.Position", "u.Roles", "u.AllowMarketing", "u.Props",
"u.NotifyProps", "u.LastPasswordUpdate", "u.LastPictureUpdate", "u.Locale", "u.Timezone", "u.RemoteId").
Distinct().
From("Users AS u").
Join("SharedChannelUsers AS scu ON u.Id = scu.UserId").
@@ -723,6 +778,19 @@ func (s SqlSharedChannelStore) UpdateUserLastSyncAt(userID string, channelID str
return nil
}
func sharedChannelAttachementFields(prefix string) []string {
if prefix != "" && !strings.HasSuffix(prefix, ".") {
prefix = prefix + "."
}
return []string{
prefix + "Id",
prefix + "FileId",
prefix + "RemoteId",
prefix + "CreateAt",
prefix + "LastSyncAt",
}
}
// SaveAttachment inserts a new shared channel file attachment record to the SharedChannelFiles table.
func (s SqlSharedChannelStore) SaveAttachment(attachment *model.SharedChannelAttachment) (*model.SharedChannelAttachment, error) {
attachment.PreSave()
@@ -731,7 +799,7 @@ func (s SqlSharedChannelStore) SaveAttachment(attachment *model.SharedChannelAtt
}
query, args, err := s.getQueryBuilder().Insert("SharedChannelAttachments").
Columns("Id", "FileId", "RemoteId", "CreateAt", "LastSyncAt").
Columns(sharedChannelAttachementFields("")...).
Values(attachment.Id, attachment.FileId, attachment.RemoteId, attachment.CreateAt, attachment.LastSyncAt).
ToSql()
if err != nil {
@@ -753,7 +821,7 @@ func (s SqlSharedChannelStore) UpsertAttachment(attachment *model.SharedChannelA
}
query := s.getQueryBuilder().
Insert("SharedChannelAttachments").
Columns("Id", "FileId", "RemoteId", "CreateAt", "LastSyncAt").
Columns(sharedChannelAttachementFields("")...).
Values(attachment.Id, attachment.FileId, attachment.RemoteId, attachment.CreateAt, attachment.LastSyncAt)
if s.DriverName() == model.DatabaseDriverMysql {
@@ -777,7 +845,7 @@ func (s SqlSharedChannelStore) GetAttachment(fileId string, remoteId string) (*m
var attachment model.SharedChannelAttachment
squery, args, err := s.getQueryBuilder().
Select("*").
Select(sharedChannelAttachementFields("")...).
From("SharedChannelAttachments").
Where(sq.Eq{"SharedChannelAttachments.FileId": fileId}).
Where(sq.Eq{"SharedChannelAttachments.RemoteId": remoteId}).

Просмотреть файл

@@ -1540,7 +1540,7 @@ func generateSearchQuery(query sq.SelectBuilder, terms []string, fields []string
} else {
searchFields = append(searchFields, fmt.Sprintf("%s LIKE ? escape '*' ", field))
}
termArgs = append(termArgs, fmt.Sprintf("%s%%", strings.TrimLeft(term, "@")))
termArgs = append(termArgs, fmt.Sprintf("%%%s%%", strings.TrimLeft(term, "@")))
}
query = query.Where(fmt.Sprintf("(%s)", strings.Join(searchFields, " OR ")), termArgs...)
}

Просмотреть файл

@@ -223,9 +223,6 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
props["BuildHash"] = model.BuildHash
props["BuildHashEnterprise"] = model.BuildHashEnterprise
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
props["BuildHashBoards"] = model.BuildHashBoards
props["BuildBoards"] = model.BuildBoards
props["BuildHashPlaybooks"] = model.BuildHashPlaybooks
props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
props["EnableFile"] = strconv.FormatBool(*c.LogSettings.EnableFile)

Просмотреть файл

@@ -109,7 +109,6 @@ services:
- "RUN_SERVER_IN_BACKGROUND=false"
- "MM_CLUSTERSETTINGS_ENABLE=true"
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
- "MM_FEATUREFLAGS_BoardsProduct=true"
networks:
- mm-test
depends_on:
@@ -147,7 +146,6 @@ services:
- "RUN_SERVER_IN_BACKGROUND=false"
- "MM_CLUSTERSETTINGS_ENABLE=true"
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
- "MM_FEATUREFLAGS_BoardsProduct=true"
networks:
- mm-test
depends_on:
@@ -185,7 +183,6 @@ services:
- "RUN_SERVER_IN_BACKGROUND=false"
- "MM_CLUSTERSETTINGS_ENABLE=true"
- "MM_CLUSTERSETTINGS_CLUSTERNAME=mm_dev_cluster"
- "MM_FEATUREFLAGS_BoardsProduct=true"
networks:
- mm-test
depends_on:

Просмотреть файл

@@ -2049,6 +2049,10 @@
"id": "api.license.request-trial.bad-request",
"translation": "The number of users requested is not correct."
},
{
"id": "api.license.request-trial.bad-request.business-email",
"translation": "Invalid business email for trial"
},
{
"id": "api.license.request-trial.bad-request.terms-not-accepted",
"translation": "You must accept the Mattermost Software Evaluation Agreement and Privacy Policy to request a license."

Просмотреть файл

@@ -341,7 +341,7 @@ func (pp *playbooksProduct) Start() error {
logrus.Info("Rudder credentials are set. Enabling analytics.")
diagnosticID := pp.serviceAdapter.GetDiagnosticID()
serverVersion := pp.serviceAdapter.GetServerVersion()
pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHashPlaybooks, serverVersion)
pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, model.BuildHash, serverVersion)
if err != nil {
return errors.Wrapf(err, "failed init telemetry client")
}
@@ -569,7 +569,7 @@ func (pp *playbooksProduct) Stop() error {
func newMetricsInstance() *metrics.Metrics {
// Init metrics
instanceInfo := metrics.InstanceInfo{
Version: model.BuildHashPlaybooks,
Version: model.BuildHash,
InstallationID: os.Getenv("MM_CLOUD_INSTALLATION_ID"),
}
return metrics.NewMetrics(instanceInfo)

Просмотреть файл

@@ -91,16 +91,10 @@ func HandleErrorWithCode(logger logrus.FieldLogger, w http.ResponseWriter, code
// ReturnJSON writes the given pointerToObject as json with the provided httpStatus
func ReturnJSON(w http.ResponseWriter, pointerToObject interface{}, httpStatus int) {
jsonBytes, err := json.Marshal(pointerToObject)
if err != nil {
logrus.WithError(err).Error("Unable to marshal JSON")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpStatus)
if _, err = w.Write(jsonBytes); err != nil {
if err := json.NewEncoder(w).Encode(pointerToObject); err != nil {
logrus.WithError(err).Warn("Unable to write to http.ResponseWriter")
return
}

Просмотреть файл

@@ -1,2 +1,3 @@
save-exact=true
legacy-peer-deps=true
global-style=true

Просмотреть файл

@@ -89,7 +89,7 @@
"unused-imports/no-unused-imports": 2,
"no-relative-import-paths/no-relative-import-paths": [
"error",
{ "allowSameFolder": true, "rootDir": "webapp/src"}
{ "allowSameFolder": true, "rootDir": "webapp/boards"}
],
/* "no-restricted-imports": ["error", {
"patterns": ["..*"]

Просмотреть файл

@@ -1,5 +1,7 @@
{
"AppBar.Tooltip": "Toggle linked boards",
"AdminBadge.SystemAdmin": "Admin",
"AdminBadge.TeamAdmin": "Team Admin",
"AppBar.Tooltip": "Toggle Linked Boards",
"Attachment.Attachment-title": "Attachment",
"AttachmentBlock.DeleteAction": "delete",
"AttachmentBlock.addElement": "add {type}",
@@ -115,7 +117,6 @@
"CenterPanel.Login": "Login",
"CenterPanel.Share": "Share",
"ChannelIntro.CreateBoard": "Create a board",
"CloudMessage.cloud-server": "Get your own free cloud server.",
"ColorOption.selectColor": "Select {color} Color",
"Comment.delete": "Delete",
"CommentsList.send": "Send",
@@ -138,6 +139,7 @@
"ContentBlock.moveDown": "Move down",
"ContentBlock.moveUp": "Move up",
"ContentBlock.text": "text",
"DateFilter.empty": "Empty",
"DateRange.clear": "Clear",
"DateRange.empty": "Empty",
"DateRange.endDate": "End date",
@@ -156,10 +158,14 @@
"Filter.ends-with": "ends with",
"Filter.includes": "includes",
"Filter.is": "is",
"Filter.is-after": "is after",
"Filter.is-before": "is before",
"Filter.is-empty": "is empty",
"Filter.is-not-empty": "is not empty",
"Filter.is-not-set": "is not set",
"Filter.is-set": "is set",
"Filter.isafter": "is after",
"Filter.isbefore": "is before",
"Filter.not-contains": "doesn't contain",
"Filter.not-ends-with": "doesn't end with",
"Filter.not-includes": "doesn't include",
@@ -243,16 +249,11 @@
"ShareBoard.userPermissionsYouText": "(You)",
"ShareTemplate.Title": "Share template",
"ShareTemplate.searchPlaceholder": "Search for people",
"Sidebar.about": "About Focalboard",
"Sidebar.add-board": "+ Add board",
"Sidebar.changePassword": "Change password",
"Sidebar.delete-board": "Delete board",
"Sidebar.duplicate-board": "Duplicate board",
"Sidebar.export-archive": "Export archive",
"Sidebar.import": "Import",
"Sidebar.import-archive": "Import archive",
"Sidebar.invite-users": "Invite users",
"Sidebar.logout": "Log out",
"Sidebar.new-category.badge": "New",
"Sidebar.new-category.drag-boards-cta": "Drag boards here...",
"Sidebar.no-boards-in-category": "No boards inside",
@@ -306,6 +307,7 @@
"ValueSelector.valueSelector": "Value selector",
"ValueSelectorLabel.openMenu": "Open menu",
"VersionMessage.help": "Check out what's new in this version.",
"VersionMessage.learn-more": "Learn more",
"View.AddView": "Add view",
"View.Board": "Board",
"View.DeleteView": "Delete view",
@@ -360,6 +362,9 @@
"WelcomePage.StartUsingIt.Text": "Start using it",
"Workspace.editing-board-template": "You're editing a board template.",
"badge.guest": "Guest",
"boardPage.confirm-join-button": "Join",
"boardPage.confirm-join-text": "You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?",
"boardPage.confirm-join-title": "Join private board",
"boardSelector.confirm-link-board": "Link board to channel",
"boardSelector.confirm-link-board-button": "Yes, link board",
"boardSelector.confirm-link-board-subtext": "When you link \"{boardName}\" to the channel, all members of the channel (existing and new) will be able to edit it. This excludes members who are guests. You can unlink a board from a channel at any time.",
@@ -374,7 +379,6 @@
"calendar.week": "Week",
"centerPanel.undefined": "No {propertyName}",
"centerPanel.unknown-user": "Unknown user",
"cloudMessage.learn-more": "Learn more",
"createImageBlock.failed": "This file couldn't be uploaded as the file size limit has been reached.",
"default-properties.badges": "Comments and description",
"default-properties.title": "Title",

68
webapp/boards/jest.config.js Обычный файл
Просмотреть файл

@@ -0,0 +1,68 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/** @type {import('jest').Config} */
const config = {
transform: {
"^.+\\.(t|j)sx?$": ["@swc/jest"]
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [
"/nanoevents/",
"node_modules/(?!react-native|react-router|react-day-picker)"
],
maxWorkers: "80%",
testEnvironment: "jsdom",
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.{ts,tsx,js,jsx}",
"!src/test/**"
],
testPathIgnorePatterns: [
"/node_modules/",
],
clearMocks: true,
coverageReporters: [
"lcov",
"text-summary"
],
moduleNameMapper: {
"^.+\\.(scss|css)$": "<rootDir>/src/test/style_mock.json",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(scss|css)$": "<rootDir>/__mocks__/styleMock.js",
"^bundle-loader\\?lazy\\!(.*)$": "$1",
"^src(.*)$": "<rootDir>/src$1",
"^i18n(.*)$": "<rootDir>/i18n$1",
"^static(.*)$": "<rootDir>/static$1",
"^moment(.*)$": "<rootDir>/../node_modules/moment$1",
},
moduleDirectories: [
"src",
"node_modules",
],
reporters: [
"default",
"jest-junit"
],
setupFiles: [
"jest-canvas-mock"
],
setupFilesAfterEnv: [
"<rootDir>/src/test/setup.tsx"
],
testTimeout: 60000,
testEnvironmentOptions: {
url: "http://localhost:8065"
}
};
module.exports = config;

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -19,10 +19,11 @@
"check-types:fix": "npm run check-types -- --noEmit --fix",
"check": "npm run check-lint && npm run check-style",
"fix": "npm run check-lint:fix && npm run check-style:fix",
"test": "jest --forceExit --detectOpenHandles --verbose",
"test:watch": "jest --watch",
"test:updatesnapshot": "jest --updateSnapshot",
"test-ci": "jest --ci --forceExit --detectOpenHandles --maxWorkers=100%",
"test": "cross-env TZ=Etc/UTC jest",
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
"test:updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot",
"test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles --verbose ",
"test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100%",
"clean": "rm -rf node_modules .eslintcache"
},
"dependencies": {
@@ -44,78 +45,26 @@
"fullcalendar": "^5.10.2",
"glob-parent": "6.0.2",
"lodash": "^4.17.21",
"marked": "^4.0.12",
"mattermost-redux": "5.33.1",
"marked": "4.0.17",
"mini-create-react-context": "^0.4.1",
"moment": "^2.29.1",
"nanoevents": "^5.1.13",
"react": "^16.13.0",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.1",
"react-day-picker": "^7.4.10",
"react-day-picker": "7.4.10",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dnd-scrolling": "^1.2.1",
"react-dnd-touch-backend": "^14.0.0",
"react-dom": "^16.13.0",
"react-dom": "17.0.2",
"react-hot-keys": "^2.7.1",
"react-hotkeys-hook": "^3.4.4",
"react-intl": "^5.20.0",
"react-redux": "^7.2.1",
"react-router-dom": "^5.2.0",
"react-select": "5.7.0",
"react-select": "5.5.9",
"trim-newlines": "^4.0.2"
},
"jest": {
"transform": {
"^.+\\.(js|tsx|ts|tsx)$": "@swc/jest"
},
"transformIgnorePatterns": [
"/nanoevents/",
"node_modules/(?!react-native|react-router|mattermost-webapp|react-day-picker)"
],
"maxWorkers": "50%",
"testEnvironment": "jsdom",
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.{ts,tsx,js,jsx}",
"!src/test/**"
],
"moduleFileExtensions": ["js", "jsx", "ts", "tsx"],
"testPathIgnorePatterns": [
"/node_modules/",
"/non_npm_dependencies/"
],
"clearMocks": true,
"coverageReporters": [
"lcov",
"text-summary"
],
"moduleNameMapper": {
"^.+\\.(scss|css)$": "<rootDir>/src/test/style_mock.json",
"^.*i18n.*\\.(json)$": "<rootDir>/src/test/i18n_mock.json",
"^bundle-loader\\?lazy\\!(.*)$": "$1",
"^react$": "<rootDir>/node_modules/react",
"^react-redux$": "<rootDir>/node_modules/react-redux",
"^react-intl$": "<rootDir>/node_modules/react-intl",
"^src(.*)$": "<rootDir>/src$1"
},
"moduleDirectories": [
"src",
"node_modules",
"non_npm_dependencies"
],
"reporters": [
"default",
"jest-junit"
],
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"<rootDir>/src/test/setup.tsx"
],
"testURL": "http://localhost:8065"
},
"devDependencies": {
"@babel/cli": "7.17.6",
"@babel/core": "7.17.8",
@@ -130,30 +79,32 @@
"@babel/runtime": "7.17.8",
"@formatjs/cli": "^4.8.2",
"@formatjs/ts-transformer": "^3.9.2",
"@swc/jest": "^0.2.24",
"@testing-library/dom": "^8.11.4",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.5.0",
"@swc/core": "1.3.40",
"@swc/jest": "0.2.24",
"@testing-library/dom": "8.20.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/draft-js": "^0.11.9",
"@types/emoji-mart": "^3.0.9",
"@types/enzyme": "3.10.11",
"@types/jest": "27.4.1",
"@types/jest": "29.4.2",
"@types/lodash": "4.14.182",
"@types/marked": "^4.0.3",
"@types/nanoevents": "^1.0.0",
"@types/node": "17.0.23",
"@types/react": "^17.0.43",
"@types/node": "16.11.7",
"@types/react": "17.0.53",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.14",
"@types/react-day-picker": "5.3.0",
"@types/react-dom": "17.0.19",
"@types/react-redux": "^7.1.23",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "4.4.4",
"@types/redux-mock-store": "1.0.3",
"@typescript-eslint/eslint-plugin": "5.16.0",
"@typescript-eslint/parser": "5.16.0",
"babel-eslint": "10.1.0",
"babel-eslint": "10.1.0",
"cross-env": "^7.0.3",
"css-loader": "6.7.1",
"eslint": "^8.11.0",
@@ -168,7 +119,7 @@
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "4.3.0",
"eslint-plugin-unused-imports": "2.0.0",
"fetch-mock-jest": "^1.5.1",
"fetch-mock-jest": "1.5.1",
"identity-obj-proxy": "3.0.0",
"image-webpack-loader": "8.1.0",
"imagemin-gifsicle": "^7.0.0",
@@ -178,10 +129,12 @@
"imagemin-svgo": "^10.0.1",
"imagemin-webp": "7.0.0",
"isomorphic-fetch": "3.0.0",
"jest": "27.5.1",
"jest-canvas-mock": "2.3.1",
"jest-junit": "13.0.0",
"jest-mock": "27.5.1",
"jest": "29.5.0",
"jest-canvas-mock": "2.4.0",
"jest-environment-jsdom": "29.5.0",
"jest-fail-on-console": "3.0.2",
"jest-junit": "15.0.0",
"jest-mock": "29.4.3",
"prettier": "^2.6.1",
"redux-mock-store": "^1.5.4",
"sass": "1.49.9",

Просмотреть файл

@@ -1,46 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`block tests correctly generate patches from two blocks should add fields on the new fields added and remove it in the undo 1`] = `
Array [
Object {
"deletedFields": Array [],
"updatedFields": Object {
[
{
"deletedFields": [],
"updatedFields": {
"newField": "new field",
},
},
Object {
"deletedFields": Array [
{
"deletedFields": [
"newField",
],
"updatedFields": Object {},
"updatedFields": {},
},
]
`;
exports[`block tests correctly generate patches from two blocks should generate two empty patches for the same block 1`] = `
Array [
Object {
"deletedFields": Array [],
"updatedFields": Object {},
[
{
"deletedFields": [],
"updatedFields": {},
},
Object {
"deletedFields": Array [],
"updatedFields": Object {},
{
"deletedFields": [],
"updatedFields": {},
},
]
`;
exports[`block tests correctly generate patches from two blocks should remove field on the new block added and add it again in the undo 1`] = `
Array [
Object {
"deletedFields": Array [
[
{
"deletedFields": [
"test",
],
"updatedFields": Object {},
"updatedFields": {},
},
Object {
"deletedFields": Array [],
"updatedFields": Object {
{
"deletedFields": [],
"updatedFields": {
"test": "test",
},
},
@@ -48,16 +48,16 @@ Array [
`;
exports[`block tests correctly generate patches from two blocks should update propertie on the main object and revert it back on the undo 1`] = `
Array [
Object {
"deletedFields": Array [],
[
{
"deletedFields": [],
"parentId": "new-parent-id",
"updatedFields": Object {},
"updatedFields": {},
},
Object {
"deletedFields": Array [],
{
"deletedFields": [],
"parentId": "old-parent-id",
"updatedFields": Object {},
"updatedFields": {},
},
]
`;

Просмотреть файл

@@ -1,52 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`board tests correctly generate patches for boards and blocks should add fields on update and remove it in the undo 1`] = `
Array [
Object {
"blockIDs": Array [
[
{
"blockIDs": [
"test-old-block-id",
],
"blockPatches": Array [
Object {
"deletedFields": Array [],
"updatedFields": Object {
"blockPatches": [
{
"deletedFields": [],
"updatedFields": {
"newField": "new field",
},
},
],
"boardIDs": Array [
"boardIDs": [
"test-board-id",
],
"boardPatches": Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
"boardPatches": [
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
],
},
Object {
"blockIDs": Array [
{
"blockIDs": [
"test-old-block-id",
],
"blockPatches": Array [
Object {
"deletedFields": Array [
"blockPatches": [
{
"deletedFields": [
"newField",
],
"updatedFields": Object {},
"updatedFields": {},
},
],
"boardIDs": Array [
"boardIDs": [
"test-board-id",
],
"boardPatches": Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
"boardPatches": [
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
],
},
@@ -54,48 +54,48 @@ Array [
`;
exports[`board tests correctly generate patches for boards and blocks should generate two empty patches for the same board and block 1`] = `
Array [
Object {
"blockIDs": Array [
[
{
"blockIDs": [
"test-card-id",
],
"blockPatches": Array [
Object {
"deletedFields": Array [],
"updatedFields": Object {},
"blockPatches": [
{
"deletedFields": [],
"updatedFields": {},
},
],
"boardIDs": Array [
"boardIDs": [
"test-board-id",
],
"boardPatches": Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
"boardPatches": [
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
],
},
Object {
"blockIDs": Array [
{
"blockIDs": [
"test-card-id",
],
"blockPatches": Array [
Object {
"deletedFields": Array [],
"updatedFields": Object {},
"blockPatches": [
{
"deletedFields": [],
"updatedFields": {},
},
],
"boardIDs": Array [
"boardIDs": [
"test-board-id",
],
"boardPatches": Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
"boardPatches": [
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
],
},
@@ -103,16 +103,16 @@ Array [
`;
exports[`board tests correctly generate patches from two boards should add card properties on the redo and remove them on the undo 1`] = `
Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [
Object {
[
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [
{
"id": "new-property-id",
"name": "property-name",
"options": Array [
Object {
"options": [
{
"color": "propColorYellow",
"id": "opt",
"value": "val",
@@ -121,30 +121,30 @@ Array [
"type": "select",
},
],
"updatedProperties": Object {},
"updatedProperties": {},
},
Object {
"deletedCardProperties": Array [
{
"deletedCardProperties": [
"new-property-id",
],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
]
`;
exports[`board tests correctly generate patches from two boards should add card properties on the redo and undo if they exists in both, but differ 1`] = `
Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [
Object {
[
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [
{
"id": "new-property-id",
"name": "property-name",
"options": Array [
Object {
"options": [
{
"color": "propColorYellow",
"id": "opt",
"value": "val",
@@ -153,17 +153,17 @@ Array [
"type": "select",
},
],
"updatedProperties": Object {},
"updatedProperties": {},
},
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [
Object {
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [
{
"id": "new-property-id",
"name": "a-different-name",
"options": Array [
Object {
"options": [
{
"color": "propColorYellow",
"id": "opt",
"value": "val",
@@ -172,22 +172,22 @@ Array [
"type": "select",
},
],
"updatedProperties": Object {},
"updatedProperties": {},
},
]
`;
exports[`board tests correctly generate patches from two boards should add card properties on the redo and undo if they exists in both, but their options are different 1`] = `
Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [
Object {
[
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [
{
"id": "new-property-id",
"name": "property-name",
"options": Array [
Object {
"options": [
{
"color": "propColorYellow",
"id": "opt",
"value": "val",
@@ -196,17 +196,17 @@ Array [
"type": "select",
},
],
"updatedProperties": Object {},
"updatedProperties": {},
},
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [
Object {
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [
{
"id": "new-property-id",
"name": "property-name",
"options": Array [
Object {
"options": [
{
"color": "propColorBrown",
"id": "another-opt",
"value": "val",
@@ -215,45 +215,45 @@ Array [
"type": "select",
},
],
"updatedProperties": Object {},
"updatedProperties": {},
},
]
`;
exports[`board tests correctly generate patches from two boards should add properties on the update patch and remove them on the undo 1`] = `
Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {
[
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {
"prop1": "val1",
},
},
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [
{
"deletedCardProperties": [],
"deletedProperties": [
"prop1",
],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
"updatedCardProperties": [],
"updatedProperties": {},
},
]
`;
exports[`board tests correctly generate patches from two boards should generate two empty patches for the same board 1`] = `
Array [
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
[
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
Object {
"deletedCardProperties": Array [],
"deletedProperties": Array [],
"updatedCardProperties": Array [],
"updatedProperties": Object {},
{
"deletedCardProperties": [],
"deletedProperties": [],
"updatedCardProperties": [],
"updatedProperties": {},
},
]
`;

Просмотреть файл

@@ -12,7 +12,7 @@ import {Utils} from './utils'
import {IPropertyTemplate} from './blocks/board'
jest.mock('./utils')
const mockedUtils = mocked(Utils, true)
const mockedUtils = mocked(Utils)
const dayMillis = 24 * 60 * 60 * 1000

Просмотреть файл

@@ -434,7 +434,7 @@ exports[`components/cardDialog limited card shows hidden view (no toolbar) 1`] =
<p
class="CardDetail__limited-body"
>
Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.
Upgrade to our Professional or Enterprise plan.
<br />
<a
class="CardDetail__limited-link"

Просмотреть файл

@@ -317,7 +317,7 @@ exports[`components/centerPanel Clicking on the Hidden card count should open a
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -982,7 +982,7 @@ exports[`components/centerPanel return centerPanel and click on card to show car
>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -1580,7 +1580,7 @@ exports[`components/centerPanel return centerPanel and click on new card to edit
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -2102,7 +2102,7 @@ exports[`components/centerPanel return centerPanel and press touch 1 with readon
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -2639,7 +2639,7 @@ exports[`components/centerPanel return centerPanel and press touch ctrl+d for on
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -3298,7 +3298,7 @@ exports[`components/centerPanel return centerPanel and press touch del for one c
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -3957,7 +3957,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -4616,7 +4616,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for one c
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -5275,7 +5275,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -5934,7 +5934,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -6593,7 +6593,7 @@ exports[`components/centerPanel return centerPanel and press touch esc for two c
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -7252,7 +7252,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -7911,7 +7911,7 @@ exports[`components/centerPanel return centerPanel and select one card and click
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -8802,7 +8802,7 @@ exports[`components/centerPanel should match snapshot for Kanban 1`] = `
>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -9322,7 +9322,7 @@ exports[`components/centerPanel should match snapshot for Kanban, not shared 1`]
>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>
@@ -9886,7 +9886,7 @@ exports[`components/centerPanel should match snapshot for Table 1`] = `
</button>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>

Просмотреть файл

@@ -54,7 +54,7 @@ exports[`/components/confirmAddUserForNotifications should match snapshot 1`] =
class="ConfirmAddUserForNotifications"
>
<p>
fake-username is not a member of the board, and will not receive any notifications about it.
fake-username isn't a member of the board, and won't receive any notifications about it.
</p>
<p>
Do you want to add fake-username to the board?

Просмотреть файл

@@ -1041,7 +1041,7 @@ exports[`properties/person show multiple, display modal 2`] = `
<span
id="aria-context"
>
option username-3 focused, 3 of 3. 1 result available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
option username-3 focused, 1 of 1. 1 result available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
</span>
</span>
<div

Просмотреть файл

@@ -16,36 +16,15 @@ exports[`components/propertyValueElement Generic fields should allow cancel 1`]
exports[`components/propertyValueElement URL fields should allow cancel 1`] = `
<div>
<div
class="URLProperty octo-propertyvalue"
class="URLProperty"
>
<a
class="link"
href="http://localhost"
rel="noreferrer"
target="_blank"
>
http://localhost
</a>
<button
aria-label="Edit"
class="IconButton Button_Edit"
title="Edit"
type="button"
>
<i
class="CompassIcon icon-pencil-outline EditIcon"
/>
</button>
<button
aria-label="Copy"
class="IconButton Button_Copy"
title="Copy"
type="button"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</button>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
style="width: 100%;"
title=""
value=""
/>
</div>
</div>
`;

Просмотреть файл

@@ -110,7 +110,7 @@ exports[`components/rhsChannelBoardItem render board with menu open 1`] = `
<div
class="menu-subtext text-75 mt-1"
>
You are not an admin of the board
You're not an admin of the board
</div>
</div>
<div

Просмотреть файл

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/components/viewMenu should match snapshot 1`] = `
Object {
{
"asFragment": [Function],
"baseElement": <body>
<div>
@@ -530,7 +530,7 @@ Object {
`;
exports[`/components/viewMenu should match snapshot, read only 1`] = `
Object {
{
"asFragment": [Function],
"baseElement": <body>
<div>

Просмотреть файл

@@ -46,7 +46,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
/>
<div>
<span>
Find Boards
Find boards
</span>
</div>
</div>
@@ -73,6 +73,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
>
<div
data-rbd-draggable-context-id="1"
data-rbd-draggable-id="categoryAttributeId1"
>
<div
class="SidebarCategory"
@@ -80,14 +81,16 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
<div
class="categoryBoardsDroppableArea"
data-rbd-droppable-context-id="1"
data-rbd-droppable-id="categoryAttributeId1"
>
<div
class="octo-sidebar-item category expanded active"
class="octo-sidebar-item category expanded "
>
<div
aria-describedby="rbd-hidden-text-1-hidden-text-5"
class="octo-sidebar-title category-title"
data-rbd-drag-handle-context-id="1"
data-rbd-drag-handle-draggable-id="categoryAttributeId1"
draggable="false"
role="button"
tabindex="0"
@@ -463,7 +466,7 @@ exports[`src/components/workspace return workspace and showcard 1`] = `
>
<span
class="Label empty "
title="Items with an empty Property 2 property will go here. This column cannot be removed."
title="Items with an empty Property 2 property will go here. This column can't be removed."
>
No Property 2
</span>
@@ -875,7 +878,7 @@ exports[`src/components/workspace return workspace readonly and showcard 1`] = `
>
<span
class="Label empty "
title="Items with an empty Property 2 property will go here. This column cannot be removed."
title="Items with an empty Property 2 property will go here. This column can't be removed."
>
No Property 2
</span>
@@ -1097,7 +1100,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
/>
<div>
<span>
Find Boards
Find boards
</span>
</div>
</div>
@@ -1124,6 +1127,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
>
<div
data-rbd-draggable-context-id="0"
data-rbd-draggable-id="categoryAttributeId1"
>
<div
class="SidebarCategory"
@@ -1131,14 +1135,16 @@ exports[`src/components/workspace should match snapshot 1`] = `
<div
class="categoryBoardsDroppableArea"
data-rbd-droppable-context-id="0"
data-rbd-droppable-id="categoryAttributeId1"
>
<div
class="octo-sidebar-item category expanded active"
class="octo-sidebar-item category expanded "
>
<div
aria-describedby="rbd-hidden-text-0-hidden-text-0"
class="octo-sidebar-title category-title"
data-rbd-drag-handle-context-id="0"
data-rbd-drag-handle-draggable-id="categoryAttributeId1"
draggable="false"
role="button"
tabindex="0"
@@ -1514,7 +1520,7 @@ exports[`src/components/workspace should match snapshot 1`] = `
>
<span
class="Label empty "
title="Items with an empty Property 2 property will go here. This column cannot be removed."
title="Items with an empty Property 2 property will go here. This column can't be removed."
>
No Property 2
</span>
@@ -1926,7 +1932,7 @@ exports[`src/components/workspace should match snapshot with readonly 1`] = `
>
<span
class="Label empty "
title="Items with an empty Property 2 property will go here. This column cannot be removed."
title="Items with an empty Property 2 property will go here. This column can't be removed."
>
No Property 2
</span>

Просмотреть файл

@@ -4,7 +4,6 @@
import React, {ReactElement, ReactNode} from 'react'
import {render, screen, waitFor} from '@testing-library/react'
import '@testing-library/jest-dom'
import {mocked} from 'jest-mock'
@@ -35,7 +34,7 @@ const wrap = (child: ReactNode): ReactElement => (
)
jest.mock('src/mutator')
const mockedMutator = mocked(mutator, true)
const mockedMutator = mocked(mutator)
describe('components/addContentMenuItem', () => {
beforeEach(() => {
@@ -66,7 +65,7 @@ describe('components/addContentMenuItem', () => {
)
expect(container).toMatchSnapshot()
const buttonElement = screen.getByRole('button', {name: 'text'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
await waitFor(() => expect(mockedMutator.insertBlock).toBeCalled())
})
@@ -82,7 +81,7 @@ describe('components/addContentMenuItem', () => {
)
expect(container).toMatchSnapshot()
const buttonElement = screen.getByRole('button', {name: 'checkbox'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
await waitFor(() => expect(mockedMutator.insertBlock).toBeCalled())
})
@@ -98,11 +97,12 @@ describe('components/addContentMenuItem', () => {
)
expect(container).toMatchSnapshot()
const buttonElement = screen.getByRole('button', {name: 'divider'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
await waitFor(() => expect(mockedMutator.insertBlock).toBeCalled())
})
test('return an error and empty element from unknown type', () => {
jest.spyOn(console, 'error').mockImplementation()
const {container} = render(
wrap(
<AddContentMenuItem
@@ -112,6 +112,8 @@ describe('components/addContentMenuItem', () => {
/>,
),
)
expect(console.error).toBeCalledWith(expect.stringContaining('addContentMenu, unknown content type: unknown'))
expect(container).toMatchSnapshot()
})
})

Просмотреть файл

@@ -2,16 +2,10 @@
// See LICENSE.txt for license information.
import React from 'react'
import {
fireEvent,
render,
screen,
act
} from '@testing-library/react'
import {fireEvent, render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import {mocked} from 'jest-mock'
@@ -27,7 +21,7 @@ const card = TestBlockFactory.createCard()
const icon = '👍'
jest.mock('src/mutator')
const mockedMutator = mocked(mutator, true)
const mockedMutator = mocked(mutator)
describe('components/blockIconSelector', () => {
beforeEach(() => {
@@ -53,14 +47,14 @@ describe('components/blockIconSelector', () => {
))
expect(container).toMatchSnapshot()
})
test('return menu on click', () => {
test('return menu on click', async () => {
const {container} = render(wrapIntl(
<BlockIconSelector
block={card}
size='l'
/>,
))
userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
await userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
expect(container).toMatchSnapshot()
})
test('return no menu in readonly', () => {
@@ -73,54 +67,50 @@ describe('components/blockIconSelector', () => {
expect(container).toMatchSnapshot()
})
test('return a new icon after click on random menu', () => {
test('return a new icon after click on random menu', async () => {
render(wrapIntl(
<BlockIconSelector
block={card}
size='l'
/>,
))
userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
await userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
const buttonRandom = screen.queryByRole('button', {name: 'Random'})
expect(buttonRandom).not.toBeNull()
userEvent.click(buttonRandom!)
await userEvent.click(buttonRandom!)
expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1)
})
test('return a new icon after click on EmojiPicker', () => {
test('return a new icon after click on EmojiPicker', async () => {
const {container, getByRole, getAllByRole} = render(wrapIntl(
<BlockIconSelector
block={card}
size='l'
/>,
))
act(() => {
userEvent.click(getByRole('button', {name: 'menuwrapper'}))
})
await userEvent.click(getByRole('button', {name: 'menuwrapper'}))
const menuPicker = container.querySelector('div#pick')
expect(menuPicker).not.toBeNull()
act(() => {
fireEvent.mouseEnter(menuPicker!)
})
fireEvent.mouseEnter(menuPicker!)
const allButtonThumbUp = getAllByRole('button', {name: /thumbsup/i})
userEvent.click(allButtonThumbUp[0])
await userEvent.click(allButtonThumbUp[0])
expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1)
expect(mockedMutator.changeBlockIcon).toBeCalledWith(card.boardId, card.id, card.fields.icon, '👍')
})
test('return no icon after click on remove menu', () => {
test('return no icon after click on remove menu', async () => {
const {container, rerender} = render(wrapIntl(
<BlockIconSelector
block={card}
size='l'
/>,
))
userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
await userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
const buttonRemove = screen.queryByRole('button', {name: 'Remove icon'})
expect(buttonRemove).not.toBeNull()
userEvent.click(buttonRemove!)
await userEvent.click(buttonRemove!)
expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1)
expect(mockedMutator.changeBlockIcon).toBeCalledWith(card.boardId, card.id, card.fields.icon, '', 'remove icon')

Просмотреть файл

@@ -14,7 +14,18 @@ exports[`components/blocksEditor/blocksEditor should match snapshot on empty 1`]
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region"
/>
>
<span
id="aria-selection"
>
option , selected.
</span>
<span
id="aria-context"
>
Select is focused ,type to refine list, press Down to open the menu,
</span>
</span>
<span
aria-atomic="false"
aria-live="polite"
@@ -22,7 +33,7 @@ exports[`components/blocksEditor/blocksEditor should match snapshot on empty 1`]
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-4bb158-control"
class=" css-1x912dq-control"
>
<div
class=" css-30zlo3-ValueContainer"
@@ -394,7 +405,18 @@ exports[`components/blocksEditor/blocksEditor should match snapshot with blocks
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region"
/>
>
<span
id="aria-selection"
>
option , selected.
</span>
<span
id="aria-context"
>
Select is focused ,type to refine list, press Down to open the menu,
</span>
</span>
<span
aria-atomic="false"
aria-live="polite"
@@ -402,7 +424,7 @@ exports[`components/blocksEditor/blocksEditor should match snapshot with blocks
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-4bb158-control"
class=" css-1x912dq-control"
>
<div
class=" css-30zlo3-ValueContainer"

Просмотреть файл

@@ -76,7 +76,18 @@ exports[`components/blocksEditor/editor should match snapshot on empty 1`] = `
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region"
/>
>
<span
id="aria-selection"
>
option , selected.
</span>
<span
id="aria-context"
>
Select is focused ,type to refine list, press Down to open the menu,
</span>
</span>
<span
aria-atomic="false"
aria-live="polite"
@@ -84,7 +95,7 @@ exports[`components/blocksEditor/editor should match snapshot on empty 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-4bb158-control"
class=" css-1x912dq-control"
>
<div
class=" css-30zlo3-ValueContainer"

Просмотреть файл

@@ -8,7 +8,18 @@ exports[`components/blocksEditor/rootInput should match Display snapshot 1`] = `
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region"
/>
>
<span
id="aria-selection"
>
option , selected.
</span>
<span
id="aria-context"
>
Select is focused ,type to refine list, press Down to open the menu,
</span>
</span>
<span
aria-atomic="false"
aria-live="polite"
@@ -16,7 +27,7 @@ exports[`components/blocksEditor/rootInput should match Display snapshot 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-4bb158-control"
class=" css-1x912dq-control"
>
<div
class=" css-30zlo3-ValueContainer"
@@ -60,7 +71,18 @@ exports[`components/blocksEditor/rootInput should match Input snapshot 1`] = `
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region"
/>
>
<span
id="aria-selection"
>
option , selected.
</span>
<span
id="aria-context"
>
Select is focused ,type to refine list, press Down to open the menu,
</span>
</span>
<span
aria-atomic="false"
aria-live="polite"
@@ -68,7 +90,7 @@ exports[`components/blocksEditor/rootInput should match Input snapshot 1`] = `
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-4bb158-control"
class=" css-1x912dq-control"
>
<div
class=" css-30zlo3-ValueContainer"
@@ -118,9 +140,18 @@ exports[`components/blocksEditor/rootInput should match Input snapshot with menu
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
>
<span
id="aria-selection"
/>
<span
id="aria-context"
>
option /title Creates a new Title block. focused, 1 of 11. 11 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
</span>
</span>
<div
class=" css-4bb158-control"
class=" css-1x912dq-control"
>
<div
class=" css-30zlo3-ValueContainer"
@@ -161,7 +192,7 @@ exports[`components/blocksEditor/rootInput should match Input snapshot with menu
/>
</div>
<div
class=" css-1aj7brc"
class=" css-u0i6pk-MenuPortal"
>
<div
class=" css-1rsmi4x-menu"

Просмотреть файл

@@ -10,7 +10,12 @@ import {
act
} from '@testing-library/react'
import {mockDOM, wrapDNDIntl, mockStateStore} from 'src/testUtils'
import {
mockDOM,
wrapDNDIntl,
mockStateStore,
setup
} from 'src/testUtils'
import {TestBlockFactory} from 'src/test/testBlockFactory'
import BlockContent from './blockContent'
@@ -141,26 +146,28 @@ describe('components/blocksEditor/blockContent', () => {
test('should call onSave on hit enter in the input', async () => {
const onSave = jest.fn()
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BlockContent
boardId='fake-board-id'
block={block}
contentOrder={[block.id]}
editing={block}
setEditing={jest.fn()}
setAfterBlock={jest.fn()}
onSave={onSave}
onMove={jest.fn()}
/>
</ReduxProvider>,
))
})
const {user} = setup(wrapDNDIntl(
<ReduxProvider store={store}>
<BlockContent
boardId='fake-board-id'
block={block}
contentOrder={[block.id]}
editing={block}
setEditing={jest.fn()}
setAfterBlock={jest.fn()}
onSave={onSave}
onMove={jest.fn()}
/>
</ReduxProvider>,
))
const input = screen.getByDisplayValue('Title')
expect(onSave).not.toBeCalled()
fireEvent.change(input, {target: {value: 'test'}})
fireEvent.keyDown(input, {key: 'Enter'})
await act(async () => {
await user.clear(input)
await user.type(input, 'test')
await user.keyboard('{Enter}')
})
expect(onSave).toBeCalledWith(expect.objectContaining({value: 'test'}))
})

Просмотреть файл

@@ -13,7 +13,7 @@ jest.mock('src/octoClient')
describe('components/blocksEditor/blocks/attachment', () => {
test('should match Display snapshot', async () => {
const mockedOcto = mocked(octoClient, true)
const mockedOcto = mocked(octoClient)
mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'})
const Component = AttachmentBlock.Display
const {container} = render(

Просмотреть файл

@@ -13,7 +13,7 @@ jest.mock('src/octoClient')
describe('components/blocksEditor/blocks/image', () => {
test('should match Display snapshot', async () => {
const mockedOcto = mocked(octoClient, true)
const mockedOcto = mocked(octoClient)
mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'})
const Component = ImageBlock.Display
const {container} = render(

Просмотреть файл

@@ -13,7 +13,7 @@ jest.mock('src/octoClient')
describe('components/blocksEditor/blocks/video', () => {
test('should match Display snapshot', async () => {
const mockedOcto = mocked(octoClient, true)
const mockedOcto = mocked(octoClient)
mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'})
const Component = VideoBlock.Display
const {container} = render(

Просмотреть файл

@@ -10,9 +10,15 @@ import {
act
} from '@testing-library/react'
import {mockDOM, wrapDNDIntl, mockStateStore} from 'src/testUtils'
import {
mockDOM,
wrapDNDIntl,
mockStateStore,
setup
} from 'src/testUtils'
import {TestBlockFactory} from 'src/test/testBlockFactory'
import {BlockData} from './blocks/types'
import BlocksEditor from './blocksEditor'
@@ -95,28 +101,25 @@ describe('components/blocksEditor/blocksEditor', () => {
test('should call onBlockCreate after introduce text and hit enter', async () => {
const onBlockCreated = jest.fn()
const {user} = setup(wrapDNDIntl(
<ReduxProvider store={store}>
<BlocksEditor
boardId='test-board'
onBlockCreated={onBlockCreated}
onBlockModified={jest.fn()}
onBlockMoved={jest.fn()}
blocks={[]}
/>
</ReduxProvider>,
))
expect(onBlockCreated).not.toBeCalled()
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BlocksEditor
boardId='test-board'
onBlockCreated={onBlockCreated}
onBlockModified={jest.fn()}
onBlockMoved={jest.fn()}
blocks={[]}
/>
</ReduxProvider>,
))
await user.type(screen.getByRole('combobox'), '/title')
await user.keyboard('{Enter}')
await user.type(screen.getByRole('textbox'), 'test')
await user.keyboard('{Enter}')
})
let input = screen.getByDisplayValue('')
expect(onBlockCreated).not.toBeCalled()
fireEvent.change(input, {target: {value: '/title'}})
fireEvent.keyDown(input, {key: 'Enter'})
input = screen.getByDisplayValue('')
fireEvent.change(input, {target: {value: 'test'}})
fireEvent.keyDown(input, {key: 'Enter'})
expect(onBlockCreated).toBeCalledWith(expect.objectContaining({value: 'test'}))
})
@@ -138,7 +141,7 @@ describe('components/blocksEditor/blocksEditor', () => {
const input = screen.getByTestId('checkbox-check')
expect(onBlockModified).not.toBeCalled()
fireEvent.click(input)
expect(onBlockModified).toBeCalledWith(expect.objectContaining({value: {checked: false, value: 'Checkbox'}}))
})
expect(onBlockModified).toBeCalledWith(expect.objectContaining({value: {checked: false, value: 'Checkbox'}}))
})
})

Просмотреть файл

@@ -3,14 +3,14 @@
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {
render,
screen,
fireEvent,
act
} from '@testing-library/react'
import {render, screen, act} from '@testing-library/react'
import {mockDOM, wrapDNDIntl, mockStateStore} from 'src/testUtils'
import {
mockDOM,
wrapDNDIntl,
mockStateStore,
setup
} from 'src/testUtils'
import {TestBlockFactory} from 'src/test/testBlockFactory'
import Editor from './editor'
@@ -82,25 +82,20 @@ describe('components/blocksEditor/editor', () => {
test('should call onSave after introduce text and hit enter', async () => {
const onSave = jest.fn()
const {user} = setup(wrapDNDIntl(
<ReduxProvider store={store}>
<Editor
boardId='fake-board-id'
onSave={onSave}
/>
</ReduxProvider>,
))
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<Editor
boardId='fake-board-id'
onSave={onSave}
/>
</ReduxProvider>,
))
await user.type(screen.getByRole('combobox'), '/title')
await user.keyboard('{Enter}')
await user.type(screen.getByRole('textbox'), 'test')
await user.keyboard('{Enter}')
})
let input = screen.getByDisplayValue('')
expect(onSave).not.toBeCalled()
fireEvent.change(input, {target: {value: '/title'}})
fireEvent.keyDown(input, {key: 'Enter'})
expect(onSave).not.toBeCalled()
input = screen.getByDisplayValue('')
fireEvent.change(input, {target: {value: 'test'}})
fireEvent.keyDown(input, {key: 'Enter'})
expect(onSave).toBeCalledWith(expect.objectContaining({value: 'test'}))
})

Просмотреть файл

@@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import Select from 'react-select'
import {CSSObject} from '@emotion/serialize'
import Select, {StylesConfig} from 'react-select'
import {getSelectBaseStyle} from 'src/theme'
@@ -16,11 +15,11 @@ type Props = {
value: string
}
const baseStyles = getSelectBaseStyle()
const baseStyles = getSelectBaseStyle<ContentType>()
const styles = {
const styles: StylesConfig<ContentType> = {
...baseStyles,
control: (provided: CSSObject): CSSObject => ({
control: (provided) => ({
...provided,
width: '100%',
height: '100%',
@@ -29,12 +28,12 @@ const styles = {
color: 'rgb(var(--center-channel-color-rgb))',
flexDirection: 'row',
}),
input: (provided: CSSObject): CSSObject => ({
input: (provided) => ({
...provided,
background: 'rgb(var(--center-channel-bg-rgb))',
color: 'rgb(var(--center-channel-color-rgb))',
}),
menu: (provided: CSSObject): CSSObject => ({
menu: (provided) => ({
...provided,
minWidth: '100%',
width: 'max-content',
@@ -42,7 +41,7 @@ const styles = {
left: '0',
marginBottom: '0',
}),
menuPortal: (provided: CSSObject): CSSObject => ({
menuPortal: (provided) => ({
...provided,
zIndex: 999,
}),
@@ -52,7 +51,7 @@ export default function RootInput(props: Props) {
const [showMenu, setShowMenu] = useState(false)
return (
<Select
<Select<ContentType>
styles={styles}
components={{DropdownIndicator: () => null, IndicatorSeparator: () => null}}
className='RootInput'
@@ -62,8 +61,8 @@ export default function RootInput(props: Props) {
menuPortalTarget={document.getElementById('focalboard-root-portal')}
menuPosition={'fixed'}
options={registry.list()}
getOptionValue={(ct: ContentType) => ct.slashCommand}
getOptionLabel={(ct: ContentType) => ct.slashCommand + ' Creates a new ' + ct.displayName + ' block.'}
getOptionValue={(ct) => ct.slashCommand}
getOptionLabel={(ct) => ct.slashCommand + ' Creates a new ' + ct.displayName + ' block.'}
filterOption={(option: any, inputValue: string): boolean => {
return inputValue.startsWith(option.value) || option.value.startsWith(inputValue)
}}
@@ -90,7 +89,7 @@ export default function RootInput(props: Props) {
props.onChange('')
}
}}
onFocus={(e: React.FocusEvent) => {
onFocus={(e) => {
const target = e.currentTarget
target.scrollIntoView({block: 'center'})
}}

Просмотреть файл

@@ -21,7 +21,7 @@ import {wrapIntl} from 'src/testUtils'
import BoardSelector from './boardSelector'
jest.mock('src/octoClient')
const mockedOctoClient = mocked(octoClient, true)
const mockedOctoClient = mocked(octoClient)
const wait = (ms: number) => {
return new Promise<void>((resolve) => {

Просмотреть файл

@@ -132,7 +132,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu
class="CompassIcon icon-kanban"
/>
<span>
Create empty board
Create an empty board
</span>
</button>
</div>
@@ -284,7 +284,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu
class="CompassIcon icon-kanban"
/>
<span>
Create empty board
Create an empty board
</span>
</button>
</div>
@@ -436,7 +436,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelector a focalboard Plu
class="CompassIcon icon-kanban"
/>
<span>
Create empty board
Create an empty board
</span>
</button>
</div>

Просмотреть файл

@@ -195,7 +195,7 @@ exports[`components/boardTemplateSelector/boardTemplateSelectorPreview should ma
>
<span
class="Label empty "
title="Items with an empty name property will go here. This column cannot be removed."
title="Items with an empty name property will go here. This column can't be removed."
>
No name
</span>

Просмотреть файл

@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
act,
render,
screen,
act,
waitFor,
within
} from '@testing-library/react'
@@ -50,11 +50,11 @@ jest.mock('src/mutator')
jest.mock('src/utils')
jest.mock('src/telemetry/telemetryClient')
const mockedTelemetry = mocked(TelemetryClient, true)
const mockedTelemetry = mocked(TelemetryClient)
describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const mockedMutator = mocked(Mutator, true)
const mockedOctoClient = mocked(client, true)
const mockedMutator = mocked(Mutator)
const mockedOctoClient = mocked(client)
const team1: Team = {
id: 'team-1',
title: 'Team 1',
@@ -194,7 +194,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
), {wrapper: MemoryRouter})
expect(container).toMatchSnapshot()
})
test('return BoardTemplateSelector and click close call the onClose callback', () => {
test('return BoardTemplateSelector and click close call the onClose callback', async () => {
const onClose = jest.fn()
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
@@ -204,10 +204,10 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
), {wrapper: MemoryRouter})
const divCloseButton = container.querySelector('div.toolbar .CloseIcon')
expect(divCloseButton).not.toBeNull()
userEvent.click(divCloseButton!)
await userEvent.click(divCloseButton!)
expect(onClose).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click new template', () => {
test('return BoardTemplateSelector and click new template', async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<BoardTemplateSelector onClose={jest.fn()}/>
@@ -216,7 +216,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
), {wrapper: MemoryRouter})
const divNewTemplate = screen.getByText('Create new template').parentElement
expect(divNewTemplate).not.toBeNull()
userEvent.click(divNewTemplate!)
await userEvent.click(divNewTemplate!)
expect(mockedMutator.addEmptyBoardTemplate).toBeCalledTimes(1)
})
test('return BoardTemplateSelector and click empty board', async () => {
@@ -230,9 +230,9 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
,
), {wrapper: MemoryRouter})
const divEmptyboard = screen.getByText('Create empty board').parentElement
const divEmptyboard = screen.getByText('Create an empty board').parentElement
expect(divEmptyboard).not.toBeNull()
userEvent.click(divEmptyboard!)
await userEvent.click(divEmptyboard!)
expect(mockedMutator.addEmptyBoard).toBeCalledTimes(1)
await waitFor(() => expect(mockedMutator.updateBoard).toBeCalledWith(newBoard, newBoard, 'linked channel'))
})
@@ -247,17 +247,13 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
), {wrapper: MemoryRouter, container: document.body.appendChild(root)})
const deleteIcon = screen.getByText(template1Title).parentElement?.querySelector('.DeleteIcon')
expect(deleteIcon).not.toBeNull()
act(() => {
userEvent.click(deleteIcon!)
})
await act(() => userEvent.click(deleteIcon!))
const {getByText} = within(root)
const deleteConfirm = getByText('Delete')
expect(deleteConfirm).not.toBeNull()
await act(async () => {
await userEvent.click(deleteConfirm!)
})
await act(() => userEvent.click(deleteConfirm!))
expect(mockedMutator.deleteBoard).toBeCalledTimes(1)
})
@@ -273,7 +269,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
))
const editIcon = screen.getByText(template1Title).parentElement?.querySelector('.EditIcon')
expect(editIcon).not.toBeNull()
userEvent.click(editIcon!)
await userEvent.click(editIcon!)
})
test('return BoardTemplateSelector and click to add board from template', async () => {
const newBoard = createBoard({id: 'new-board'} as Board)
@@ -288,15 +284,12 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const divBoardToSelect = screen.getByText(template1Title).parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
await userEvent.click(divBoardToSelect!)
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await userEvent.click(useTemplateButton!)
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '1', team1.id))
@@ -319,15 +312,13 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const divBoardToSelect = screen.getByText(template1Title).parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
await userEvent.click(divBoardToSelect!)
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await userEvent.click(useTemplateButton!)
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '1', team1.id))
@@ -347,15 +338,13 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const divBoardToSelect = screen.getByText(globalTemplateTitle).parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
await userEvent.click(divBoardToSelect!)
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await userEvent.click(useTemplateButton!)
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), 'global-1', team1.id))
await waitFor(() => expect(mockedTelemetry.trackEvent).toBeCalledWith('boards', 'createBoardViaTemplate', {boardTemplateId: 'template_id_global'}))
@@ -374,16 +363,13 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
const divBoardToSelect = screen.getByText('Welcome to Boards!').parentElement
expect(divBoardToSelect).not.toBeNull()
act(() => {
userEvent.click(divBoardToSelect!)
})
await userEvent.click(divBoardToSelect!)
const useTemplateButton = screen.getByText('Use this template').parentElement
expect(useTemplateButton).not.toBeNull()
act(() => {
userEvent.click(useTemplateButton!)
})
await userEvent.click(useTemplateButton!)
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledTimes(1))
await waitFor(() => expect(mockedMutator.addBoardFromTemplate).toBeCalledWith(team1.id, expect.anything(), expect.anything(), expect.anything(), '2', team1.id))
await waitFor(() => expect(mockedTelemetry.trackEvent).toBeCalledWith('boards', 'createBoardViaTemplate', {boardTemplateId: 'template_id_2'}))

Просмотреть файл

@@ -199,7 +199,7 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
</ReduxProvider>
,
))
userEvent.click(container.querySelector('.BoardTemplateSelectorItem')!)
await userEvent.click(container.querySelector('.BoardTemplateSelectorItem')!)
expect(onSelect).toBeCalledTimes(1)
expect(onSelect).toBeCalledWith(template)
expect(onDelete).not.toBeCalled()
@@ -222,7 +222,7 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
</ReduxProvider>
,
))
userEvent.click(container.querySelector('.BoardTemplateSelectorItem .EditIcon')!)
await userEvent.click(container.querySelector('.BoardTemplateSelectorItem .EditIcon')!)
expect(onEdit).toBeCalledTimes(1)
expect(onEdit).toBeCalledWith(template.id)
expect(onSelect).not.toBeCalled()
@@ -248,16 +248,12 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
</ReduxProvider>
,
), {container: document.body.appendChild(root)})
act(() => {
userEvent.click(root.querySelector('.BoardTemplateSelectorItem .DeleteIcon')!)
})
await act(() => userEvent.click(root.querySelector('.BoardTemplateSelectorItem .DeleteIcon')!))
expect(root).toMatchSnapshot()
const {getByText} = within(root)
act(() => {
userEvent.click(getByText('Delete')!)
})
await act(() => userEvent.click(getByText('Delete')!))
await waitFor(async () => expect(onDelete).toBeCalledTimes(1))
await waitFor(async () => expect(onDelete).toBeCalledWith(template))

Просмотреть файл

@@ -96,3 +96,9 @@ exports[`components/boardsUnfurl/BoardsUnfurl renders when limited 1`] = `
</a>
</div>
`;
exports[`components/boardsUnfurl/BoardsUnfurl test invalid card, invalid block 1`] = `<div />`;
exports[`components/boardsUnfurl/BoardsUnfurl test invalid card, valid block 1`] = `<div />`;
exports[`components/boardsUnfurl/BoardsUnfurl test no card 1`] = `<div />`;

Просмотреть файл

@@ -16,12 +16,14 @@ import {createBoard} from 'src/blocks/board'
import octoClient from 'src/octoClient'
import {wrapIntl} from 'src/testUtils'
import {createBoardView} from 'src/blocks/boardView'
import BoardsUnfurl from './boardsUnfurl'
jest.mock('src/octoClient')
jest.mock('src/utils')
const mockedOctoClient = mocked(octoClient, true)
const mockedUtils = mocked(Utils, true)
const mockedOctoClient = mocked(octoClient)
const mockedUtils = mocked(Utils)
mockedUtils.createGuid = jest.requireActual('src/utils').Utils.createGuid
mockedUtils.blockTypeToIDType = jest.requireActual('src/utils').Utils.blockTypeToIDType
mockedUtils.displayDateTime = jest.requireActual('src/utils').Utils.displayDateTime
@@ -114,5 +116,118 @@ describe('components/boardsUnfurl/BoardsUnfurl', () => {
expect(container).toMatchSnapshot()
})
it('test no card', async () => {
const mockStore = configureStore([])
const store = mockStore({
language: {
value: 'en',
},
teams: {
allTeams: [team],
current: team,
},
})
const board = {...createBoard(), title: 'test board'}
// mockedOctoClient.getBoard.mockResolvedValueOnce(board)
const component = (
<ReduxProvider store={store}>
{wrapIntl(
<BoardsUnfurl
embed={{data: JSON.stringify({workspaceID: 'foo', cardID: '', boardID: board.id, readToken: 'abc', originalPath: '/test'})}}
/>,
)}
</ReduxProvider>
)
let container: Element | DocumentFragment | null = null
await act(async () => {
const result = render(component)
container = result.container
})
expect(container).toMatchSnapshot()
})
it('test invalid card, valid block', async () => {
const mockStore = configureStore([])
const store = mockStore({
language: {
value: 'en',
},
teams: {
allTeams: [team],
current: team,
},
})
const cards = [{...createBoardView(), title: 'test view', updateAt: 12345}]
const board = {...createBoard(), title: 'test board'}
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce(cards)
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
const component = (
<ReduxProvider store={store}>
{wrapIntl(
<BoardsUnfurl
embed={{data: JSON.stringify({workspaceID: 'foo', cardID: cards[0].id, boardID: board.id, readToken: 'abc', originalPath: '/test'})}}
/>,
)}
</ReduxProvider>
)
let container: Element | DocumentFragment | null = null
await act(async () => {
const result = render(component)
container = result.container
})
expect(mockedOctoClient.getBoard).toBeCalledWith(board.id)
expect(mockedOctoClient.getBlocksWithBlockID).toBeCalledWith(cards[0].id, board.id, 'abc')
expect(container).toMatchSnapshot()
})
it('test invalid card, invalid block', async () => {
const mockStore = configureStore([])
const store = mockStore({
language: {
value: 'en',
},
teams: {
allTeams: [team],
current: team,
},
})
const board = {...createBoard(), title: 'test board'}
mockedOctoClient.getBlocksWithBlockID.mockResolvedValueOnce([])
mockedOctoClient.getBoard.mockResolvedValueOnce(board)
const component = (
<ReduxProvider store={store}>
{wrapIntl(
<BoardsUnfurl
embed={{data: JSON.stringify({workspaceID: 'foo', cardID: 'invalidCard', boardID: board.id, readToken: 'abc', originalPath: '/test'})}}
/>,
)}
</ReduxProvider>
)
let container: Element | DocumentFragment | null = null
await act(async () => {
const result = render(component)
container = result.container
})
expect(mockedOctoClient.getBoard).toBeCalledWith(board.id)
expect(mockedOctoClient.getBlocksWithBlockID).toBeCalledWith('invalidCard', board.id, 'abc')
expect(container).toMatchSnapshot()
})
})

Просмотреть файл

@@ -84,7 +84,7 @@ export const BoardsUnfurl = (props: Props): JSX.Element => {
],
)
const [firstCard] = cards as Card[]
if (!firstCard || !fetchedBoard) {
if (!firstCard || !fetchedBoard || firstCard.type !== 'card') {
setLoading(false)
return null
}
@@ -116,7 +116,7 @@ export const BoardsUnfurl = (props: Props): JSX.Element => {
useWebsockets(currentTeamId, (wsClient: WSClient) => {
const onChangeHandler = (_: WSClient, blocks: Block[]): void => {
const cardBlock: Block|undefined = blocks.find((b) => b.id === cardID)
if (cardBlock && !cardBlock.deleteAt) {
if (cardBlock && !cardBlock.deleteAt && cardBlock.type === 'card') {
setCard(cardBlock as Card)
}

Просмотреть файл

@@ -143,7 +143,7 @@ describe('components/calculations/Calculation', () => {
expect(container).toMatchSnapshot()
})
test('should match snapshot - option change', () => {
test('should match snapshot - option change', async () => {
const onMenuOpen = jest.fn()
const onMenuClose = jest.fn()
const onChange = jest.fn()
@@ -171,7 +171,7 @@ describe('components/calculations/Calculation', () => {
)
const countMenuOption = container.querySelector('#react-select-2-option-1')
userEvent.click(countMenuOption as Element)
await userEvent.click(countMenuOption as Element)
expect(container).toMatchSnapshot()
expect(onMenuOpen).not.toBeCalled()
expect(onMenuClose).toBeCalled()

Просмотреть файл

@@ -2,9 +2,8 @@
// See LICENSE.txt for license information.
import React from 'react'
import Select, {components, DropdownIndicatorProps} from 'react-select'
import Select, {components, DropdownIndicatorProps, StylesConfig} from 'react-select'
import {CSSObject} from '@emotion/serialize'
import {useIntl, IntlShape} from 'react-intl'
@@ -114,22 +113,22 @@ function generateTypesByOption(): Map<string, string[]> {
return mapping
}
const baseStyles = getSelectBaseStyle()
const baseStyles = getSelectBaseStyle<Option>()
const styles = {
const styles: StylesConfig<Option> = {
...baseStyles,
dropdownIndicator: (provided: CSSObject): CSSObject => ({
...baseStyles.dropdownIndicator(provided),
dropdownIndicator: (...props) => ({
...baseStyles.dropdownIndicator?.(...props),
pointerEvents: 'none',
}),
control: (): CSSObject => ({
control: () => ({
border: 0,
width: '100%',
margin: '0',
display: 'flex',
flexDirection: 'row',
}),
menu: (provided: CSSObject): CSSObject => ({
menu: (provided) => ({
...provided,
minWidth: '100%',
width: 'max-content',
@@ -137,15 +136,15 @@ const styles = {
left: '0',
marginBottom: '0',
}),
singleValue: (provided: CSSObject): CSSObject => ({
...baseStyles.singleValue(provided),
singleValue: (...props) => ({
...baseStyles.singleValue?.(...props),
opacity: '0.8',
fontSize: '12px',
right: '0',
textTransform: 'uppercase',
}),
valueContainer: (provided: CSSObject): CSSObject => ({
...baseStyles.valueContainer(provided),
valueContainer: (...props) => ({
...baseStyles.valueContainer?.(...props),
display: 'none',
pointerEvents: 'none',
}),
@@ -178,7 +177,7 @@ export const CalculationOptions = (props: BaseCalculationOptionProps): JSX.Eleme
const intl = useIntl()
return (
<Select
<Select<Option>
styles={styles}
value={Options[props.value]}
isMulti={false}

Просмотреть файл

@@ -5,7 +5,6 @@ import {render} from '@testing-library/react'
import {Provider as ReduxProvider} from 'react-redux'
import {TestBlockFactory} from 'src/test/testBlockFactory'
import '@testing-library/jest-dom'
import {wrapIntl, mockStateStore} from 'src/testUtils'
import {IPropertyTemplate} from 'src/blocks/board'

Просмотреть файл

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/jest-dom'
import {act, render} from '@testing-library/react'
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'

Просмотреть файл

@@ -5,7 +5,6 @@ import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {render, screen} from '@testing-library/react'
import '@testing-library/jest-dom'
import {TestBlockFactory} from 'src/test/testBlockFactory'
import {blocksById, mockStateStore, wrapDNDIntl} from 'src/testUtils'

Просмотреть файл

@@ -171,7 +171,7 @@ exports[`components/cardDetail/CardDetail should render hidden view if limited 1
<p
class="CardDetail__limited-body"
>
Upgrade to our Professional or Enterprise plan to view archived cards, have unlimited views per boards, unlimited cards and more.
Upgrade to our Professional or Enterprise plan.
<br />
<a
class="CardDetail__limited-link"
@@ -480,7 +480,7 @@ exports[`components/cardDetail/CardDetail should show add properties tour tip 1`
<div
class="tutorial-tour-tip__body"
>
Add various properties to cards to make them more powerful!
Add various properties to cards to make them more powerful.
</div>
<div
class="tutorial-tour-tip__image"

Просмотреть файл

@@ -437,7 +437,7 @@ exports[`components/cardDetail/cardDetailContentsMenu return cardDetailContentsM
>
<div
aria-label="menuwrapper"
class="MenuWrapper override menuOpened"
class="MenuWrapper"
role="button"
>
<button
@@ -448,198 +448,6 @@ exports[`components/cardDetail/cardDetailContentsMenu return cardDetailContentsM
Add content
</span>
</button>
<div
class="Menu noselect top "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="text"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="menu-option__icon"
>
<svg
class="TextIcon Icon"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M432 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
/>
</svg>
</div>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
text
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="image"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="menu-option__icon"
>
<svg
class="ImageIcon Icon"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M464 64H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V112c0-26.51-21.49-48-48-48zm-6 336H54a6 6 0 0 1-6-6V118a6 6 0 0 1 6-6h404a6 6 0 0 1 6 6v276a6 6 0 0 1-6 6zM128 152c-22.091 0-40 17.909-40 40s17.909 40 40 40 40-17.909 40-40-17.909-40-40-40zM96 352h320v-80l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L192 304l-39.515-39.515c-4.686-4.686-12.284-4.686-16.971 0L96 304v48z"
/>
</svg>
</div>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
image
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="divider"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="menu-option__icon"
>
<svg
class="DividerIcon Icon"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 432,224 H 16 c -8.836556,0 -16,7.16344 -16,16 v 32 c 0,8.83656 7.163444,16 16,16 h 416 c 8.83656,0 16,-7.16344 16,-16 v -32 c 0,-8.83656 -7.16344,-16 -16,-16 z"
/>
</svg>
</div>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
divider
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="checkbox"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="menu-option__icon"
>
<svg
class="CheckIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="20,60 40,80 80,40"
/>
</svg>
</div>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
checkbox
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Cancel
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

Просмотреть файл

@@ -10,7 +10,7 @@ exports[`components/cardDetail/CommentsList comments show up 1`] = `
>
<img
class="comment-avatar"
src="data:image/svg+xml,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 100 100\\" style=\\"fill: rgb(192, 192, 192);\\"><rect width=\\"100\\" height=\\"100\\" /></svg>"
src="data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>"
/>
<div
class="MarkdownEditor octo-editor newcomment "
@@ -34,7 +34,7 @@ exports[`components/cardDetail/CommentsList comments show up 1`] = `
>
<img
class="comment-avatar"
src="data:image/svg+xml,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 100 100\\" style=\\"fill: rgb(192, 192, 192);\\"><rect width=\\"100\\" height=\\"100\\" /></svg>"
src="data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>"
/>
<div
class="comment-username"
@@ -80,7 +80,7 @@ exports[`components/cardDetail/CommentsList comments show up 1`] = `
>
<img
class="comment-avatar"
src="data:image/svg+xml,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 100 100\\" style=\\"fill: rgb(192, 192, 192);\\"><rect width=\\"100\\" height=\\"100\\" /></svg>"
src="data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>"
/>
<div
class="comment-username"
@@ -138,7 +138,7 @@ exports[`components/cardDetail/CommentsList comments show up in readonly mode 1`
>
<img
class="comment-avatar"
src="data:image/svg+xml,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 100 100\\" style=\\"fill: rgb(192, 192, 192);\\"><rect width=\\"100\\" height=\\"100\\" /></svg>"
src="data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>"
/>
<div
class="comment-username"
@@ -170,7 +170,7 @@ exports[`components/cardDetail/CommentsList comments show up in readonly mode 1`
>
<img
class="comment-avatar"
src="data:image/svg+xml,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 100 100\\" style=\\"fill: rgb(192, 192, 192);\\"><rect width=\\"100\\" height=\\"100\\" /></svg>"
src="data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>"
/>
<div
class="comment-username"

Просмотреть файл

@@ -26,7 +26,7 @@ import CardDetail from './cardDetail'
global.fetch = FetchMock.fn
jest.mock('src/octoClient')
const mockedOctoClient = mocked(octoClient, true)
const mockedOctoClient = mocked(octoClient)
beforeEach(() => {
FetchMock.fn.mockReset()
@@ -299,7 +299,7 @@ describe('components/cardDetail/CardDetail', () => {
expect(nextBtn).toBeDefined()
expect(nextBtn).not.toBeNull()
await act(async () => {
userEvent.click(nextBtn!)
await userEvent.click(nextBtn!)
})
expect(mockedOctoClient.patchUserConfig).toBeCalledWith(
'user_id_1',
@@ -408,7 +408,7 @@ describe('components/cardDetail/CardDetail', () => {
expect(nextBtn).toBeDefined()
expect(nextBtn).not.toBeNull()
await act(async () => {
userEvent.click(nextBtn!)
await userEvent.click(nextBtn!)
})
expect(mockedOctoClient.patchUserConfig).toBeCalledWith(
'user_id_1',
@@ -521,7 +521,7 @@ describe('components/cardDetail/CardDetail', () => {
expect(nextBtn).toBeDefined()
expect(nextBtn).not.toBeNull()
await act(async () => {
userEvent.click(nextBtn!)
await userEvent.click(nextBtn!)
})
expect(mockedOctoClient.patchUserConfig).toBeCalledWith(
'user_id_1',

Просмотреть файл

@@ -1,11 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act, render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {render, screen} from '@testing-library/react'
import React, {ReactElement, ReactNode} from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {wrapIntl, mockStateStore} from 'src/testUtils'
import userEvent from '@testing-library/user-event'
import {wrapIntl, mockStateStore, setup} from 'src/testUtils'
import {TestBlockFactory} from 'src/test/testBlockFactory'
@@ -36,22 +37,18 @@ describe('components/cardDetail/cardDetailContentsMenu', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('return cardDetailContentsMenu', () => {
test('return cardDetailContentsMenu', async () => {
const {container} = render(wrap(<CardDetailContentsMenu/>))
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
})
test('return cardDetailContentsMenu and add Text content', async () => {
const {container} = render(wrap(<CardDetailContentsMenu/>))
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
userEvent.click(buttonElement)
const {user, container} = setup(wrap(<CardDetailContentsMenu/>))
await user.click(screen.getByRole('button', {name: 'menuwrapper'}))
expect(container).toMatchSnapshot()
await act(async () => {
const buttonAddText = screen.getByRole('button', {name: 'text'})
userEvent.click(buttonAddText)
})
await user.click(screen.getByRole('button', {name: 'text'}))
expect(container).toMatchSnapshot()
})
})

Просмотреть файл

@@ -10,7 +10,6 @@ import {
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {mocked} from 'jest-mock'
import '@testing-library/jest-dom'
import {createIntl} from 'react-intl'
import configureStore from 'redux-mock-store'
@@ -25,7 +24,7 @@ import {PropertyType} from 'src/properties/types'
import CardDetailProperties from './cardDetailProperties'
jest.mock('src/mutator')
const mockedMutator = mocked(mutator, true)
const mockedMutator = mocked(mutator)
describe('components/cardDetail/CardDetailProperties', () => {
const board = TestBlockFactory.createBoard()
@@ -140,25 +139,25 @@ describe('components/cardDetail/CardDetailProperties', () => {
expect(container).toMatchSnapshot()
})
it('should show confirmation dialog when deleting existing select property', () => {
it('should show confirmation dialog when deleting existing select property', async () => {
renderComponent()
const menuElement = screen.getByRole('button', {name: 'Owner'})
userEvent.click(menuElement)
await act(() => userEvent.click(menuElement))
const deleteButton = screen.getByRole('button', {name: /delete/i})
userEvent.click(deleteButton)
await act(() => userEvent.click(deleteButton))
expect(screen.getByRole('heading', {name: 'Confirm delete property'})).toBeInTheDocument()
expect(screen.getByRole('button', {name: /delete/i})).toBeInTheDocument()
})
it('should show property types menu', () => {
it('should show property types menu', async () => {
const intl = createIntl({locale: 'en'})
const {container} = renderComponent()
const menuElement = screen.getByRole('button', {name: /add a property/i})
userEvent.click(menuElement)
await act(() => userEvent.click(menuElement))
expect(container).toMatchSnapshot()
const selectProperty = screen.getByText(/select property type/i)
@@ -170,11 +169,11 @@ describe('components/cardDetail/CardDetailProperties', () => {
})
})
it('should allow change property types menu, confirm', () => {
it('should allow change property types menu, confirm', async () => {
renderComponent()
const menuElement = screen.getByRole('button', {name: 'Owner'})
userEvent.click(menuElement)
await act(() => userEvent.click(menuElement))
const typeProperty = screen.getByText(/Type: Select/i)
expect(typeProperty).toBeInTheDocument()
@@ -182,7 +181,7 @@ describe('components/cardDetail/CardDetailProperties', () => {
fireEvent.mouseOver(typeProperty)
const newTypeMenu = screen.getByRole('button', {name: 'Text'})
userEvent.click(newTypeMenu)
await act(() => userEvent.click(newTypeMenu))
expect(screen.getByRole('heading', {name: 'Confirm property type change'})).toBeInTheDocument()
expect(screen.getByRole('button', {name: /Change property/i})).toBeInTheDocument()
@@ -192,7 +191,7 @@ describe('components/cardDetail/CardDetailProperties', () => {
const result = renderComponent()
// rename to "Owner-Renamed"
onPropertyRenameNoConfirmationDialog(result.container)
await onPropertyRenameNoConfirmationDialog(result.container)
const propertyTemplate = board.cardProperties[0]
// should be called once on confirming renaming the property
@@ -204,12 +203,10 @@ describe('components/cardDetail/CardDetailProperties', () => {
renderComponent()
const menuElement = screen.getByRole('button', {name: /add a property/i})
userEvent.click(menuElement)
await act(() => userEvent.click(menuElement))
const numberType = screen.getByRole('button', {name: /number/i})
await act(async () => {
const numberType = screen.getByRole('button', {name: /number/i})
userEvent.click(numberType)
})
await act( () => userEvent.click(numberType))
expect(mockedMutator.insertPropertyTemplate).toHaveBeenCalledTimes(1)
@@ -220,11 +217,11 @@ describe('components/cardDetail/CardDetailProperties', () => {
expect(template!.type).toBe('number')
})
it('confirmation on delete dialog should delete the property', () => {
it('confirmation on delete dialog should delete the property', async () => {
const result = renderComponent()
const container = result.container
openDeleteConfirmationDialog(container)
await openDeleteConfirmationDialog(container)
const propertyTemplate = board.cardProperties[0]
@@ -232,48 +229,49 @@ describe('components/cardDetail/CardDetailProperties', () => {
expect(confirmButton).toBeDefined()
//click delete button
userEvent.click(confirmButton!)
await act(() => userEvent.click(confirmButton!))
// should be called once on confirming delete
expect(mockedMutator.deleteProperty).toBeCalledTimes(1)
expect(mockedMutator.deleteProperty).toBeCalledWith(board, views, cards, propertyTemplate.id)
})
it('cancel on delete dialog should do nothing', () => {
it('cancel on delete dialog should do nothing', async () => {
const result = renderComponent()
const container = result.container
openDeleteConfirmationDialog(container)
await openDeleteConfirmationDialog(container)
const cancelButton = result.getByTitle('Cancel')
expect(cancelButton).toBeDefined()
userEvent.click(cancelButton!)
await act(() => userEvent.click(cancelButton!))
expect(container).toMatchSnapshot()
})
function openDeleteConfirmationDialog(container: HTMLElement) {
async function openDeleteConfirmationDialog(container: HTMLElement) {
const propertyLabel = container.querySelector('.MenuWrapper')
expect(propertyLabel).toBeDefined()
userEvent.click(propertyLabel!)
await act(() => userEvent.click(propertyLabel!))
const deleteOption = container.querySelector('.MenuOption.TextOption')
expect(propertyLabel).toBeDefined()
userEvent.click(deleteOption!)
await act(() => userEvent.click(deleteOption!))
const confirmDialog = container.querySelector('.dialog.confirmation-dialog-box')
expect(confirmDialog).toBeDefined()
}
function onPropertyRenameNoConfirmationDialog(container: HTMLElement) {
async function onPropertyRenameNoConfirmationDialog(container: HTMLElement) {
const propertyLabel = container.querySelector('.MenuWrapper')
expect(propertyLabel).toBeDefined()
userEvent.click(propertyLabel!)
await act(() => userEvent.click(propertyLabel!))
// write new name in the name text box
const propertyNameInput = container.querySelector('.PropertyMenu.menu-textbox')
expect(propertyNameInput).toBeDefined()
userEvent.type(propertyNameInput!, 'Owner - Renamed{enter}')
userEvent.click(propertyLabel!)
await act(() => userEvent.type(propertyNameInput!, 'Owner - Renamed{enter}', {initialSelectionStart: 0, initialSelectionEnd: 5}))
await act(() => userEvent.click(propertyLabel!))
}
})

Просмотреть файл

@@ -17,7 +17,7 @@ import mutator from 'src/mutator'
import Comment from './comment'
jest.mock('src/mutator')
const mockedMutator = mocked(mutator, true)
const mockedMutator = mocked(mutator)
const board = TestBlockFactory.createBoard()
const card = TestBlockFactory.createCard(board)
@@ -50,7 +50,7 @@ describe('components/cardDetail/comment', () => {
}
})
test('return comment', () => {
test('return comment', async () => {
const {container} = render(wrapIntl(
<ReduxProvider store={store}>
<Comment
@@ -62,7 +62,7 @@ describe('components/cardDetail/comment', () => {
</ReduxProvider>,
))
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
})
@@ -80,7 +80,7 @@ describe('components/cardDetail/comment', () => {
expect(container).toMatchSnapshot()
})
test('return comment and delete comment', () => {
test('return comment and delete comment', async () => {
const {container} = render(wrapIntl(
<ReduxProvider store={store}>
<Comment
@@ -92,15 +92,15 @@ describe('components/cardDetail/comment', () => {
</ReduxProvider>,
))
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
userEvent.click(buttonDelete)
await userEvent.click(buttonDelete)
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
expect(mockedMutator.deleteBlock).toBeCalledWith(comment)
})
test('return guest comment', () => {
test('return guest comment', async () => {
const localStore = mockStateStore([], {users: {boardUsers: {[comment.modifiedBy]: {username: 'username_1', is_guest: true}}}})
const {container} = render(wrapIntl(
<ReduxProvider store={localStore}>
@@ -113,11 +113,11 @@ describe('components/cardDetail/comment', () => {
</ReduxProvider>,
))
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
})
test('return guest comment readonly', () => {
test('return guest comment readonly', async () => {
const localStore = mockStateStore([], {users: {boardUsers: {[comment.modifiedBy]: {username: 'username_1', is_guest: true}}}})
const {container} = render(wrapIntl(
<ReduxProvider store={localStore}>
@@ -132,7 +132,7 @@ describe('components/cardDetail/comment', () => {
expect(container).toMatchSnapshot()
})
test('return guest comment and delete comment', () => {
test('return guest comment and delete comment', async () => {
const localStore = mockStateStore([], {users: {boardUsers: {[comment.modifiedBy]: {username: 'username_1', is_guest: true}}}})
const {container} = render(wrapIntl(
<ReduxProvider store={localStore}>
@@ -145,10 +145,10 @@ describe('components/cardDetail/comment', () => {
</ReduxProvider>,
))
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
expect(container).toMatchSnapshot()
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
userEvent.click(buttonDelete)
await userEvent.click(buttonDelete)
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
expect(mockedMutator.deleteBlock).toBeCalledWith(comment)
})

Просмотреть файл

@@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/jest-dom'
import {act, render, screen} from '@testing-library/react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
@@ -23,9 +22,9 @@ jest.mock('src/octoClient')
jest.mock('src/utils')
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
const mockedUtils = mocked(Utils, true)
const mockedMutator = mocked(mutator, true)
const mockedOctoClient = mocked(octoClient, true)
const mockedUtils = mocked(Utils)
const mockedMutator = mocked(mutator)
const mockedOctoClient = mocked(octoClient)
mockedUtils.createGuid.mockReturnValue('test-id')
beforeAll(() => {
@@ -94,135 +93,116 @@ describe('components/cardDialog', () => {
jest.clearAllMocks()
})
test('should match snapshot', async () => {
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
expect(container).toMatchSnapshot()
})
test('should match snapshot without permissions', async () => {
let container
const localStore = mockStateStore([], {...state, teams: {current: undefined}})
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={localStore}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={localStore}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
expect(container).toMatchSnapshot()
})
test('return a cardDialog readonly', async () => {
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={true}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={true}
/>
</ReduxProvider>,
))
expect(container).toMatchSnapshot()
})
test('return cardDialog and do a close action', async () => {
const closeFn = jest.fn()
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={closeFn}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={closeFn}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
const buttonElement = screen.getByRole('button', {name: 'Close dialog'})
userEvent.click(buttonElement)
await userEvent.click(buttonElement)
expect(closeFn).toBeCalledTimes(1)
})
test('return cardDialog menu content', async () => {
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
userEvent.click(buttonMenu)
await userEvent.click(buttonMenu)
expect(container).toMatchSnapshot()
})
test('return cardDialog menu content and verify delete action', async () => {
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
userEvent.click(buttonMenu)
await userEvent.click(buttonMenu)
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
userEvent.click(buttonDelete)
await userEvent.click(buttonDelete)
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
expect(confirmDialog).toBeDefined()
@@ -231,36 +211,32 @@ describe('components/cardDialog', () => {
expect(confirmButton).toBeDefined()
//click delete button
userEvent.click(confirmButton!)
await userEvent.click(confirmButton!)
// should be called once on confirming delete
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
})
test('return cardDialog menu content and cancel delete confirmation do nothing', async () => {
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
userEvent.click(buttonMenu)
await userEvent.click(buttonMenu)
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
userEvent.click(buttonDelete)
await userEvent.click(buttonDelete)
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
expect(confirmDialog).toBeDefined()
@@ -269,57 +245,53 @@ describe('components/cardDialog', () => {
expect(cancelButton).toBeDefined()
//click delete button
userEvent.click(cancelButton!)
await userEvent.click(cancelButton!)
// should do nothing on cancel delete dialog
expect(container).toMatchSnapshot()
})
test('return cardDialog menu content and do a New template from card', async () => {
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
userEvent.click(buttonMenu)
await userEvent.click(buttonMenu)
const buttonTemplate = screen.getByRole('button', {name: 'New template from card'})
userEvent.click(buttonTemplate)
await userEvent.click(buttonTemplate)
expect(mockedMutator.duplicateCard).toBeCalledTimes(1)
})
test('return cardDialog menu content and do a copy Link', async () => {
await act(async () => {
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
})
render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
userEvent.click(buttonMenu)
await userEvent.click(buttonMenu)
const buttonCopy = screen.getByRole('button', {name: 'Copy link'})
userEvent.click(buttonCopy)
await userEvent.click(buttonCopy)
expect(mockedUtils.copyTextToClipboard).toBeCalledTimes(1)
})
@@ -338,24 +310,21 @@ describe('components/cardDialog', () => {
const newStore = mockStateStore([], newState)
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={newStore}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={newStore}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
expect(container).toMatchSnapshot()
})
@@ -373,24 +342,21 @@ describe('components/cardDialog', () => {
const newStore = mockStateStore([], newState)
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={newStore}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[limitedCard]}
cardId={limitedCard.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const {container} = render(wrapDNDIntl(
<ReduxProvider store={newStore}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[limitedCard]}
cardId={limitedCard.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
expect(container).toMatchSnapshot()
})
})

Просмотреть файл

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
act,
fireEvent,
render,
screen,
within,
act
within
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
@@ -38,9 +38,9 @@ jest.mock('src/octoClient')
jest.mock('src/mutator')
jest.mock('src/telemetry/telemetryClient')
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
const mockedUtils = mocked(Utils, true)
const mockedMutator = mocked(Mutator, true)
const mockedOctoClient = mocked(octoClient, true)
const mockedUtils = mocked(Utils)
const mockedMutator = mocked(Mutator)
const mockedOctoClient = mocked(octoClient)
mockedUtils.createGuid.mockReturnValue('test-id')
mockedUtils.generateClassName = jest.requireActual('src/utils').Utils.generateClassName
describe('components/centerPanel', () => {
@@ -234,7 +234,7 @@ describe('components/centerPanel', () => {
expect(container).toMatchSnapshot()
})
describe('return centerPanel and', () => {
test('select one card and click background', () => {
test('select one card and click background', async () => {
activeView.fields.viewType = 'table'
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
@@ -255,13 +255,13 @@ describe('components/centerPanel', () => {
//select card
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement).not.toBeNull()
userEvent.click(cardElement, {shiftKey: true})
fireEvent.click(cardElement, {shiftKey: true})
expect(container).toMatchSnapshot()
//background
const boardElement = container.querySelector('.BoardComponent')
expect(boardElement).not.toBeNull()
userEvent.click(boardElement!)
fireEvent.click(boardElement!)
expect(container).toMatchSnapshot()
})
@@ -288,7 +288,7 @@ describe('components/centerPanel', () => {
expect(container).toMatchSnapshot()
})
test('press touch esc for one card selected', () => {
test('press touch esc for one card selected', async () => {
activeView.fields.viewType = 'table'
const {container, baseElement} = render(wrapDNDIntl(
<ReduxProvider store={store}>
@@ -306,11 +306,9 @@ describe('components/centerPanel', () => {
</ReduxProvider>,
))
act(() => {
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement.parentNode).not.toBeNull()
userEvent.click(cardElement as HTMLElement, {shiftKey: true})
})
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement.parentNode).not.toBeNull()
fireEvent.click(cardElement, {shiftKey: true})
expect(container).toMatchSnapshot()
//escape
@@ -335,27 +333,23 @@ describe('components/centerPanel', () => {
</ReduxProvider>,
))
act(() => {
//select card1
const card1Element = screen.getByRole('textbox', {name: 'card1'})
expect(card1Element).not.toBeNull()
userEvent.click(card1Element, {shiftKey: true})
})
//select card1
const card1Element = screen.getByRole('textbox', {name: 'card1'})
expect(card1Element).not.toBeNull()
fireEvent.click(card1Element, {shiftKey: true})
expect(container).toMatchSnapshot()
act(() => {
//select card2
const card2Element = screen.getByRole('textbox', {name: 'card2'})
expect(card2Element).not.toBeNull()
userEvent.click(card2Element, {shiftKey: true, ctrlKey: true})
})
//select card2
const card2Element = screen.getByRole('textbox', {name: 'card2'})
expect(card2Element).not.toBeNull()
fireEvent.click(card2Element, {shiftKey: true, ctrlKey: true})
expect(container).toMatchSnapshot()
//escape
fireEvent.keyDown(baseElement, {keyCode: 27})
expect(container).toMatchSnapshot()
})
test('press touch del for one card selected', () => {
test('press touch del for one card selected', async () => {
activeView.fields.viewType = 'table'
const {container, baseElement} = render(wrapDNDIntl(
<ReduxProvider store={store}>
@@ -372,19 +366,18 @@ describe('components/centerPanel', () => {
/>
</ReduxProvider>,
))
act(() => {
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement).not.toBeNull()
userEvent.click(cardElement, {shiftKey: true})
})
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement).not.toBeNull()
fireEvent.click(cardElement, {shiftKey: true})
expect(container).toMatchSnapshot()
//delete
fireEvent.keyDown(baseElement, {keyCode: 8})
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
})
test('press touch ctrl+d for one card selected', () => {
test('press touch ctrl+d for one card selected', async () => {
activeView.fields.viewType = 'table'
const {container, baseElement} = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CenterPanel
@@ -400,18 +393,16 @@ describe('components/centerPanel', () => {
/>
</ReduxProvider>,
))
act(() => {
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement).not.toBeNull()
userEvent.click(cardElement, {shiftKey: true})
})
const cardElement = screen.getByRole('textbox', {name: 'card1'})
expect(cardElement).not.toBeNull()
fireEvent.click(cardElement, {shiftKey: true})
expect(container).toMatchSnapshot()
//ctrl+d
fireEvent.keyDown(baseElement, {ctrlKey: true, keyCode: 68})
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
})
test('click on card to show card', () => {
test('click on card to show card', async () => {
activeView.fields.viewType = 'board'
const mockedShowCard = jest.fn()
const {container} = render(wrapDNDIntl(
@@ -433,11 +424,11 @@ describe('components/centerPanel', () => {
const kanbanCardElements = container.querySelectorAll('.KanbanCard')
expect(kanbanCardElements).not.toBeNull()
const kanbanCardElement = kanbanCardElements[0]
userEvent.click(kanbanCardElement)
await userEvent.click(kanbanCardElement)
expect(container).toMatchSnapshot()
expect(mockedShowCard).toBeCalledWith(card1.id)
})
test('click on new card to add card', () => {
test('click on new card to add card', async () => {
activeView.fields.viewType = 'table'
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
@@ -456,10 +447,10 @@ describe('components/centerPanel', () => {
))
const buttonWithMenuElement = container.querySelector('.ButtonWithMenu')
expect(buttonWithMenuElement).not.toBeNull()
userEvent.click(buttonWithMenuElement!)
await act(() => userEvent.click(buttonWithMenuElement!))
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
})
test('click on new card to add card template', () => {
test('click on new card to add card template', async () => {
activeView.fields.viewType = 'table'
const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}>
@@ -478,13 +469,13 @@ describe('components/centerPanel', () => {
))
const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
expect(elementMenuWrapper).not.toBeNull()
userEvent.click(elementMenuWrapper!)
await act(() => userEvent.click(elementMenuWrapper!))
const buttonNewTemplate = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'New template'})
userEvent.click(buttonNewTemplate)
await act(() => userEvent.click(buttonNewTemplate))
expect(mockedMutator.insertBlock).toBeCalledTimes(1)
})
test('click on new card to add card from template', () => {
test('click on new card to add card from template', async () => {
activeView.fields.viewType = 'table'
activeView.fields.defaultTemplateId = '1'
const {container} = render(wrapDNDIntl(
@@ -504,14 +495,14 @@ describe('components/centerPanel', () => {
))
const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
expect(elementMenuWrapper).not.toBeNull()
userEvent.click(elementMenuWrapper!)
await act(() => userEvent.click(elementMenuWrapper!))
const elementCard1 = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'card1'})
expect(elementCard1).not.toBeNull()
userEvent.click(elementCard1)
await act(() => userEvent.click(elementCard1))
expect(mockedMutator.performAsUndoGroup).toBeCalledTimes(1)
})
test('click on new card to edit template', () => {
test('click on new card to edit template', async () => {
activeView.fields.viewType = 'table'
activeView.fields.defaultTemplateId = '1'
const {container} = render(wrapDNDIntl(
@@ -531,15 +522,15 @@ describe('components/centerPanel', () => {
))
const elementMenuWrapper = container.querySelector('.ButtonWithMenu > div.MenuWrapper')
expect(elementMenuWrapper).not.toBeNull()
userEvent.click(elementMenuWrapper!)
await act(() => userEvent.click(elementMenuWrapper!))
const elementCard1 = within(elementMenuWrapper!.parentElement!).getByRole('button', {name: 'card1'})
expect(elementCard1).not.toBeNull()
const elementMenuWrapperCard1 = within(elementCard1).getByRole('button', {name: 'menuwrapper'})
expect(elementMenuWrapperCard1).not.toBeNull()
userEvent.click(elementMenuWrapperCard1)
await act(() => userEvent.click(elementMenuWrapperCard1))
const elementEditMenuTemplate = within(elementMenuWrapperCard1).getByRole('button', {name: 'Edit'})
expect(elementMenuWrapperCard1).not.toBeNull()
userEvent.click(elementEditMenuTemplate)
await act(() => userEvent.click(elementEditMenuTemplate))
expect(container).toMatchSnapshot()
})
})

Просмотреть файл

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react'
import {Post} from 'mattermost-redux/types/posts'
import {Post} from '@mattermost/types/posts'
const PostTypeCloudUpgradeNudge = (props: {post: Post}): JSX.Element => {
const ctaHandler = (e: React.MouseEvent) => {

Просмотреть файл

@@ -1,6 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/jest-dom'
import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
@@ -28,7 +27,7 @@ describe('/components/confirmAddUserForNotifications', () => {
expect(result.container).toMatchSnapshot()
})
it('confirm button click, run onConfirm Function once', () => {
it('confirm button click, run onConfirm Function once', async () => {
const onConfirm = jest.fn()
const result = render(
@@ -42,11 +41,11 @@ describe('/components/confirmAddUserForNotifications', () => {
/>,
),
)
userEvent.click(result.getByTitle('Add to board'))
await userEvent.click(result.getByTitle('Add to board'))
expect(onConfirm).toBeCalledTimes(1)
})
it('cancel button click runs onClose function', () => {
it('cancel button click runs onClose function', async () => {
const onClose = jest.fn()
const result = render(
@@ -60,7 +59,7 @@ describe('/components/confirmAddUserForNotifications', () => {
/>,
),
)
userEvent.click(result.getByTitle('Cancel'))
await userEvent.click(result.getByTitle('Cancel'))
expect(onClose).toBeCalledTimes(1)
})
})

Просмотреть файл

@@ -1,6 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/jest-dom'
import {act, render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import React from 'react'
@@ -55,16 +54,16 @@ describe('/components/confirmationDialogBox', () => {
expect(containerWithCnfrmBtnText).toMatchSnapshot()
})
it('confirm button click, run onConfirm Function once', () => {
it('confirm button click, run onConfirm Function once', async () => {
const result = render(
wrapDNDIntl(<ConfirmationDialogBox dialogBox={dialogProps}/>),
)
userEvent.click(result.getByTitle('Confirm'))
await userEvent.click(result.getByTitle('Confirm'))
expect(dialogProps.onConfirm).toBeCalledTimes(1)
})
it('confirm button (with passed prop text), run onConfirm Function once', () => {
it('confirm button (with passed prop text), run onConfirm Function once', async () => {
const resultWithConfirmBtnText = render(
wrapDNDIntl(
<ConfirmationDialogBox
@@ -73,21 +72,21 @@ describe('/components/confirmationDialogBox', () => {
),
)
userEvent.click(
await userEvent.click(
resultWithConfirmBtnText.getByTitle(dialogPropsWithCnfrmBtnText.confirmButtonText),
)
expect(dialogPropsWithCnfrmBtnText.onConfirm).toBeCalledTimes(1)
})
it('cancel button click runs onClose function', () => {
it('cancel button click runs onClose function', async () => {
const result = render(wrapDNDIntl(
<ConfirmationDialogBox
dialogBox={dialogProps}
/>,
))
userEvent.click(result.getByTitle('Cancel'))
await userEvent.click(result.getByTitle('Cancel'))
expect(dialogProps.onClose).toBeCalledTimes(1)
})
})

Просмотреть файл

@@ -15,7 +15,6 @@ exports[`components/content/checkboxElement should change title 1`] = `
placeholder="Edit text..."
spellcheck="true"
title="new title"
type="text"
value="new title"
/>
</div>

Просмотреть файл

@@ -16,7 +16,7 @@ import {IUser} from 'src/user'
import AttachmentElement from './attachmentElement'
jest.mock('src/octoClient')
const mockedOcto = mocked(octoClient, true)
const mockedOcto = mocked(octoClient)
mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.txt'})
mockedOcto.getFileInfo.mockResolvedValue({
name: 'test.txt',

Просмотреть файл

@@ -8,7 +8,6 @@ import {
screen,
waitFor
} from '@testing-library/react'
import '@testing-library/jest-dom'
import {mocked} from 'jest-mock'
import userEvent from '@testing-library/user-event'
@@ -21,7 +20,7 @@ import mutator from 'src/mutator'
import CheckboxElement from './checkboxElement'
jest.mock('src/mutator')
const mockedMutator = mocked(mutator, true)
const mockedMutator = mocked(mutator)
const board = TestBlockFactory.createBoard()
const card = TestBlockFactory.createCard(board)
@@ -84,7 +83,7 @@ describe('components/content/checkboxElement', () => {
expect(container).toMatchSnapshot()
})
it('should change title', () => {
it('should change title', async () => {
const {container} = render(wrap(
<CheckboxElement
block={checkboxBlock}
@@ -93,8 +92,8 @@ describe('components/content/checkboxElement', () => {
))
const newTitle = 'new title'
const input = screen.getByRole('textbox', {name: /test-title/i})
userEvent.clear(input)
userEvent.type(input, newTitle)
await userEvent.clear(input)
await userEvent.type(input, newTitle)
fireEvent.blur(input)
expect(container).toMatchSnapshot()
expect(mockedMutator.changeBlockTitle).toHaveBeenCalledTimes(1)
@@ -106,7 +105,7 @@ describe('components/content/checkboxElement', () => {
expect.anything())
})
it('should toggle value', () => {
it('should toggle value', async () => {
const {container} = render(wrap(
<CheckboxElement
block={checkboxBlock}
@@ -114,7 +113,7 @@ describe('components/content/checkboxElement', () => {
/>,
))
const input = screen.getByRole('checkbox')
userEvent.click(input)
await userEvent.click(input)
expect(container).toMatchSnapshot()
expect(mockedMutator.updateBlock).toHaveBeenCalledTimes(1)
expect(mockedMutator.updateBlock).toHaveBeenCalledWith(
@@ -149,17 +148,17 @@ describe('components/content/checkboxElement', () => {
const input = screen.getByRole('textbox', {name: /test-title/i})
// should not add new checkbox when current one has empty title
userEvent.clear(input)
userEvent.type(input, '{enter}')
await userEvent.clear(input)
await userEvent.type(input, '{Enter}')
expect(addElement).toHaveBeenCalledTimes(0)
// should add new checkbox when current one has non-empty title
userEvent.clear(input)
userEvent.type(input, 'new-title{enter}')
await userEvent.clear(input)
await userEvent.type(input, 'new-title{Enter}')
await waitFor(() => expect(addElement).toHaveBeenCalledTimes(1))
})
it('should delete automatically added checkbox with empty title on esc/enter pressed', () => {
it('should delete automatically added checkbox with empty title on esc/enter pressed', async () => {
const addedBlock = createContentBlock(checkboxBlock)
addedBlock.title = ''
const deleteElement = jest.fn()
@@ -173,15 +172,16 @@ describe('components/content/checkboxElement', () => {
/>
</CardDetailContext.Provider>,
))
const input = screen.getByRole('textbox')
userEvent.type(input, '{esc}')
// should delete if title is empty
await userEvent.type(input, '{Escape}')
expect(deleteElement).toHaveBeenCalledTimes(1)
userEvent.type(input, '{enter}')
await userEvent.type(input, '{Enter}')
expect(deleteElement).toHaveBeenCalledTimes(2)
// should not delete if title is not empty
userEvent.type(input, 'new-title{esc}')
await userEvent.type(input, 'new-title{Enter}')
expect(deleteElement).toHaveBeenCalledTimes(2)
})
})

Просмотреть файл

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React, {ReactElement, ReactNode} from 'react'
import '@testing-library/jest-dom'
import {render} from '@testing-library/react'
@@ -54,6 +53,8 @@ describe('components/content/contentElement', () => {
})
it('should return null for unknown type', () => {
jest.spyOn(console, 'error').mockImplementation()
const block: ContentBlock = {...contentBlock, type: 'unknown'}
const {container} = render(wrap(
<ContentElement
@@ -63,5 +64,6 @@ describe('components/content/contentElement', () => {
/>,
))
expect(container).toBeEmptyDOMElement()
expect(console.error).toBeCalledWith(expect.stringContaining('ContentElement, unknown content type: unknown'))
})
})

Просмотреть файл

@@ -17,7 +17,7 @@ import octoClient from 'src/octoClient'
import ImageElement from './imageElement'
jest.mock('src/octoClient')
const mockedOcto = mocked(octoClient, true)
const mockedOcto = mocked(octoClient)
mockedOcto.getFileAsDataUrl.mockResolvedValue({url: 'test.jpg'})
describe('components/content/ImageElement', () => {

Просмотреть файл

@@ -5,7 +5,6 @@ import React from 'react'
import {render, act} from '@testing-library/react'
import {Provider as ReduxProvider} from 'react-redux'
import '@testing-library/jest-dom'
import {mocked} from 'jest-mock'
@@ -22,7 +21,7 @@ import TextElement from './textElement'
jest.mock('src/utils')
jest.mock('src/mutator')
jest.mock('draft-js/lib/generateRandomKey', () => () => '123')
const mockedUtils = mocked(Utils, true)
const mockedUtils = mocked(Utils)
mockedUtils.createGuid.mockReturnValue('test-id')
const defaultBlock: TextBlock = {
id: 'test-id',

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше