Просмотр исходного кода

Merge branch 'master' into imprv/improve-test-for-updatepage

yohei0125 4 лет назад
Родитель
Сommit
4dce826b6b

+ 2 - 0
.github/workflows/ci-app-prod.yml

@@ -24,6 +24,7 @@ jobs:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     with:
       node-version: 16.x
+      skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
       cypress-report-artifact-name: Cypress report
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
@@ -38,6 +39,7 @@ jobs:
 
     with:
       node-version: 16.x
+      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
       cypress-report-artifact-name: Cypress report
     secrets:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}

+ 1 - 1
.github/workflows/ci-app.yml

@@ -102,7 +102,7 @@ jobs:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
       - name: Upload coverage report as artifact
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: Coverage Report
           path: packages/app/coverage

+ 3 - 2
.github/workflows/pr-to-master.yml

@@ -13,7 +13,8 @@ jobs:
   auto-labeling:
     runs-on: ubuntu-latest
 
-    if: ${{ !contains(github.event.pull_request.labels.*.name, 'exclude from changelog') }}
+    if: |
+      !contains(github.event.pull_request.labels.*.name, 'exclude from changelog')
 
     steps:
       - uses: release-drafter/release-drafter@v5
@@ -30,7 +31,7 @@ jobs:
         !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
-      - uses: amannn/action-semantic-pull-request@v3.4.5
+      - uses: amannn/action-semantic-pull-request@v4.2.0
         with:
           types: |
             feat

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -81,7 +81,7 @@ jobs:
         mv /tmp/.buildx-cache-new /tmp/.buildx-cache
 
     - name: Add tag
-      uses: anothrNick/github-tag-action@1.36.0
+      uses: anothrNick/github-tag-action@1.38.0
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         CUSTOM_TAG: v${{ steps.package-json.outputs.packageVersion }}

+ 5 - 5
.github/workflows/reusable-app-prod.yml

@@ -63,13 +63,13 @@ jobs:
         echo ::set-output name=file::production.tar
 
     - name: Upload production files as artifact
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: Production Files
         path: ${{ steps.archive-prod-files.outputs.file }}
 
     - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: Bundle Analyzing Report
         path: packages/app/report/bundle-analyzer.html
@@ -140,7 +140,7 @@ jobs:
         npx lerna bootstrap -- --production
 
     - name: Download production files artifact
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: Production Files
 
@@ -219,7 +219,7 @@ jobs:
         npx lerna bootstrap -- --frozen-lockfile
 
     - name: Download production files artifact
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: Production Files
 
@@ -258,7 +258,7 @@ jobs:
 
     - name: Upload results
       if: always()
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: ${{ inputs.cypress-report-artifact-name }}
         path: |

+ 5 - 1
.github/workflows/reusable-app-reg-suit.yml

@@ -9,6 +9,8 @@ on:
       checkout-ref:
         type: string
         default: ${{ github.head_ref }}
+      skip-reg-suit:
+        type: boolean
       cypress-report-artifact-name:
         required: true
         type: string
@@ -33,6 +35,8 @@ jobs:
     # https://github.com/weseek/growi/settings/environments/376165508/edit
     environment: VRT
 
+    if: ${{ !inputs.skip-reg-suit }}
+
     env:
       REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
       AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -71,7 +75,7 @@ jobs:
         npx lerna bootstrap -- --frozen-lockfile
 
     - name: Download screenshots taken by cypress
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: ${{ inputs.cypress-report-artifact-name }}
         path: packages/app/test/cypress

+ 3 - 0
packages/app/src/components/Me/PasswordSettings.jsx

@@ -99,6 +99,9 @@ class PasswordSettings extends React.Component {
           <div className="row mb-3">
             <label htmlFor="oldPassword" className="col-md-3 text-md-right">{ t('personal_settings.current_password') }</label>
             <div className="col-md-5">
+              {/* to prevent autocomplete username into userForm[email] in BasicInfoSettings component */}
+              {/* https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion */}
+              <input type="password" autoComplete="new-password" style={{ display: 'none' }} />
               <input
                 className="form-control"
                 type="password"

+ 10 - 11
packages/app/src/components/PageList/PageListItemL.tsx

@@ -3,24 +3,18 @@ import React, {
   ForwardRefRenderFunction, memo, useCallback, useImperativeHandle, useRef, useEffect,
 } from 'react';
 
-import { useTranslation } from 'react-i18next';
-import { CustomInput } from 'reactstrap';
 
-import Clamp from 'react-multiline-clamp';
+import { DevidedPagePath } from '@growi/core';
+import { UserPicture, PageListMeta } from '@growi/ui';
 import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
+import Clamp from 'react-multiline-clamp';
+import { CustomInput } from 'reactstrap';
 import urljoin from 'url-join';
 
-import { UserPicture, PageListMeta } from '@growi/ui';
-import { DevidedPagePath } from '@growi/core';
-
-import { useSWRxPageInfo } from '../../stores/page';
 
 import { ISelectable } from '~/client/interfaces/selectable-all';
 import { bookmark, unbookmark } from '~/client/services/page-operation';
-import { useIsDeviceSmallerThanLg } from '~/stores/ui';
-import {
-  usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
-} from '~/stores/modal';
 import {
   IPageInfoAll, IPageInfoForEntity, IPageInfoForListing, IPageWithMeta, isIPageInfoForListing, isIPageInfoForEntity,
 } from '~/interfaces/page';
@@ -29,7 +23,12 @@ import {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
+import {
+  usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
+} from '~/stores/modal';
+import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 
+import { useSWRxPageInfo } from '../../stores/page';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
 

+ 2 - 0
packages/app/src/components/Sidebar/Tag.tsx

@@ -1,5 +1,7 @@
 import React, { FC, useState, useEffect } from 'react';
+
 import { useTranslation } from 'react-i18next';
+
 import TagsList from '../TagsList';
 
 const Tag: FC = () => {

+ 2 - 6
packages/app/src/components/TagCloudBox.tsx

@@ -2,14 +2,10 @@ import React, { FC } from 'react';
 
 import { TagCloud } from 'react-tagcloud';
 
-type Tag = {
-  _id: string,
-  name: string,
-  count: number,
-}
+import { ITagHasCount } from '~/interfaces/tag';
 
 type Props = {
-  tags:Tag[],
+  tags: ITagHasCount[],
   minSize?: number,
   maxSize?: number,
 }

+ 0 - 126
packages/app/src/components/TagsList.jsx

@@ -1,126 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-
-import PaginationWrapper from './PaginationWrapper';
-import TagCloudBox from './TagCloudBox';
-import { apiGet } from '../client/util/apiv1-client';
-import { toastError } from '../client/util/apiNotification';
-
-class TagsList extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      tagData: [],
-      activePage: 1,
-      totalTags: 0,
-      pagingLimit: 10,
-    };
-
-    this.handlePage = this.handlePage.bind(this);
-    this.getTagList = this.getTagList.bind(this);
-  }
-
-  async componentWillMount() {
-    await this.getTagList(1);
-  }
-
-  async componentDidUpdate() {
-    if (this.props.isOnReload) {
-      await this.getTagList(this.state.activePage);
-    }
-  }
-
-  async handlePage(selectedPage) {
-    await this.getTagList(selectedPage);
-  }
-
-  async getTagList(selectPageNumber) {
-    const limit = this.state.pagingLimit;
-    const offset = (selectPageNumber - 1) * limit;
-    let res;
-
-    try {
-      res = await apiGet('/tags.list', { limit, offset });
-    }
-    catch (error) {
-      toastError(error);
-    }
-
-    const totalTags = res.totalCount;
-    const tagData = res.data;
-    const activePage = selectPageNumber;
-
-    this.setState({
-      tagData,
-      activePage,
-      totalTags,
-    });
-  }
-
-  /**
-   * generate Elements of Tag
-   *
-   * @param {any} pages Array of pages Model Obj
-   *
-   */
-  generateTagList(tagData) {
-    return tagData.map((data) => {
-      return (
-        <a key={data.name} href={`/_search?q=tag:${data.name}`} className="list-group-item">
-          <i className="icon-tag mr-2"></i>{data.name}
-          <span className="ml-4 list-tag-count badge badge-secondary text-muted">{data.count}</span>
-        </a>
-      );
-    });
-  }
-
-  render() {
-    const { t } = this.props;
-    const messageForNoTag = this.state.tagData.length ? null : <h3>{ t('You have no tag, You can set tags on pages') }</h3>;
-
-    return (
-      <>
-        <header className="py-0">
-          <h1 className="title text-center mt-5 mb-3 font-weight-bold">{`${t('Tags')}(${this.state.totalTags})`}</h1>
-        </header>
-        <div className="row text-center">
-          <div className="col-12 mb-5 px-5">
-            <TagCloudBox tags={this.state.tagData} minSize={20} />
-          </div>
-          <div className="col-12 tag-list mb-4">
-            <ul className="list-group text-left">
-              {this.generateTagList(this.state.tagData)}
-            </ul>
-            {messageForNoTag}
-          </div>
-          <div className="col-12 tag-list-pagination">
-            <PaginationWrapper
-              activePage={this.state.activePage}
-              changePage={this.handlePage}
-              totalItemsCount={this.state.totalTags}
-              pagingLimit={this.state.pagingLimit}
-              align="center"
-              size="md"
-            />
-          </div>
-        </div>
-      </>
-    );
-  }
-
-}
-
-TagsList.propTypes = {
-  isOnReload: PropTypes.bool,
-  t: PropTypes.func.isRequired, // i18next
-};
-
-TagsList.defaultProps = {
-  isOnReload: false,
-};
-
-export default withTranslation()(TagsList);

+ 85 - 0
packages/app/src/components/TagsList.tsx

@@ -0,0 +1,85 @@
+import React, { FC, useEffect, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { useSWRxTagsList } from '~/stores/tag';
+
+import PaginationWrapper from './PaginationWrapper';
+import TagCloudBox from './TagCloudBox';
+
+
+const PAGING_LIMIT = 10;
+
+type Props = {
+  isOnReload: boolean
+}
+
+const TagsList: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+
+  const [activePage, setActivePage] = useState<number>(1);
+  const [pagingOffset, setPagingOffset] = useState<number>(0);
+
+  const { data: tagsList, error, mutate } = useSWRxTagsList(PAGING_LIMIT, pagingOffset);
+
+  const handlePage = (selectedPageNumber: number) => {
+    setActivePage(selectedPageNumber);
+    setPagingOffset((selectedPageNumber - 1) * PAGING_LIMIT);
+  };
+
+  useEffect(() => {
+    if (props.isOnReload) {
+      mutate();
+    }
+  }, [mutate, props.isOnReload]);
+
+  const isLoading = tagsList === undefined && error == null;
+  if (isLoading) {
+    return (
+      <div className="text-muted text-center">
+        <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
+      </div>
+    );
+  }
+
+  return (
+    <>
+      <header className="py-0">
+        <h1 className="title text-center mt-5 mb-3 font-weight-bold">{`${t('Tags')}(${tagsList?.totalCount || 0})`}</h1>
+      </header>
+      <div className="row text-center">
+        <div className="col-12 mb-5 px-5">
+          <TagCloudBox tags={tagsList?.data || []} minSize={20} />
+        </div>
+        <div className="col-12 tag-list mb-4">
+          <ul className="list-group text-left">
+            {
+              tagsList?.data != null && tagsList.data.length > 0
+                ? tagsList.data.map((tag) => {
+                  return (
+                    <a key={tag.name} href={`/_search?q=tag:${tag.name}`} className="list-group-item">
+                      <i className="icon-tag mr-2"></i>{tag.name}
+                      <span className="ml-4 list-tag-count badge badge-secondary text-muted">{tag.count}</span>
+                    </a>
+                  );
+                })
+                : <h3>{ t('You have no tag, You can set tags on pages') }</h3>
+            }
+          </ul>
+        </div>
+        <div className="col-12 tag-list-pagination">
+          <PaginationWrapper
+            activePage={activePage}
+            changePage={handlePage}
+            totalItemsCount={tagsList?.totalCount || 0}
+            pagingLimit={PAGING_LIMIT}
+            align="center"
+            size="md"
+          />
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default TagsList;

+ 8 - 0
packages/app/src/interfaces/tag.ts

@@ -3,7 +3,15 @@ export type ITag = {
   createdAt: Date;
 }
 
+export type ITagHasCount = ITag & { count: number }
+
 export type ITagsSearchApiv1Result = {
   ok: boolean,
   tags: string[]
 }
+
+export type ITagsListApiv1Result = {
+  ok: boolean,
+  data: ITagHasCount[],
+  totalCount: number,
+}

+ 14 - 0
packages/app/src/stores/tag.tsx

@@ -0,0 +1,14 @@
+import { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
+
+import { ITagsListApiv1Result } from '~/interfaces/tag';
+
+import { apiGet } from '../client/util/apiv1-client';
+
+
+export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<ITagsListApiv1Result, Error> => {
+  return useSWRImmutable(
+    ['/tags.list', limit, offset],
+    (endpoint, limit, offset) => apiGet(endpoint, { limit, offset }).then((result: ITagsListApiv1Result) => result),
+  );
+};

+ 14 - 16
packages/app/src/stores/ui.tsx

@@ -1,32 +1,30 @@
 import { RefObject } from 'react';
+
+import { isClient, pagePathUtils } from '@growi/core';
+import { Breakpoint, addBreakpointListener } from '@growi/ui';
+import SimpleBar from 'simplebar-react';
 import {
   useSWRConfig, SWRResponse, Key, Fetcher,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
-import SimpleBar from 'simplebar-react';
-
-import { Breakpoint, addBreakpointListener } from '@growi/ui';
-import { pagePathUtils } from '@growi/core';
 
+import { IFocusable } from '~/client/interfaces/focusable';
+import { Nullable } from '~/interfaces/common';
 import { SidebarContentsType } from '~/interfaces/ui';
+import { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 
-import { useStaticSWR } from './use-static-swr';
 import {
   useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage,
   useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath, useIsNotFoundPermalink,
 } from './context';
-import { IFocusable } from '~/client/interfaces/focusable';
-import { Nullable } from '~/interfaces/common';
-import { UpdateDescCountData } from '~/interfaces/websocket';
+import { useStaticSWR } from './use-static-swr';
 
 const { isSharedPage } = pagePathUtils;
 
 const logger = loggerFactory('growi:stores:ui');
 
-const isServer = typeof window === 'undefined';
-
 
 /** **********************************************************
  *                          Unions
@@ -55,10 +53,10 @@ export const useSidebarScrollerRef = (initialData?: RefObject<SimpleBar>): SWRRe
  *********************************************************** */
 
 export const useIsMobile = (): SWRResponse<boolean, Error> => {
-  const key = isServer ? null : 'isMobile';
+  const key = isClient() ? 'isMobile' : null;
 
   let configuration;
-  if (!isServer) {
+  if (isClient()) {
     const userAgent = window.navigator.userAgent.toLowerCase();
     configuration = {
       fallbackData: /iphone|ipad|android/.test(userAgent),
@@ -166,11 +164,11 @@ export const useEditorMode = (): SWRResponse<EditorMode, Error> => {
 };
 
 export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
-  const key: Key = isServer ? null : 'isDeviceSmallerThanMd';
+  const key: Key = isClient() ? 'isDeviceSmallerThanMd' : null;
 
   const { cache, mutate } = useSWRConfig();
 
-  if (!isServer) {
+  if (isClient()) {
     const mdOrAvobeHandler = function(this: MediaQueryList): void {
       // sm -> md: matches will be true
       // md -> sm: matches will be false
@@ -190,11 +188,11 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
 };
 
 export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
-  const key: Key = isServer ? null : 'isDeviceSmallerThanLg';
+  const key: Key = isClient() ? 'isDeviceSmallerThanLg' : null;
 
   const { cache, mutate } = useSWRConfig();
 
-  if (!isServer) {
+  if (isClient()) {
     const lgOrAvobeHandler = function(this: MediaQueryList): void {
       // md -> lg: matches will be true
       // lg -> md: matches will be false

+ 3 - 2
packages/app/src/utils/project-dir-utils.ts

@@ -4,8 +4,9 @@ import fs from 'fs';
 import path from 'path';
 import process from 'process';
 
-const isServer = typeof window === 'undefined';
-const isCurrentDirRoot = isServer && fs.existsSync('./next.config.js');
+import { isServer } from '@growi/core';
+
+const isCurrentDirRoot = isServer() && fs.existsSync('./next.config.js');
 
 export const projectRoot = isCurrentDirRoot
   ? process.cwd()

+ 2 - 0
packages/app/test/cypress/integration/3-search/search.spec.ts

@@ -13,6 +13,7 @@ context('Access to search result page', () => {
   it('/_search with "q" param is successfully loaded', () => {
     cy.visit('/_search', { qs: { q: 'labels alerts cards blocks' } });
 
+    cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
 
@@ -24,6 +25,7 @@ context('Access to search result page', () => {
 
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
+    cy.getByTestid('search-result-content').should('be.visible');
 
     cy.getByTestid('cb-select').first().click({force: true});
     cy.screenshot(`${ssPrefix}-the-first-checkbox-on`);

+ 20 - 7
packages/app/test/cypress/integration/6-home/home.spec.ts

@@ -6,6 +6,8 @@ context('Access Home', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   it('Visit home', () => {
@@ -29,6 +31,8 @@ context('Access User settings', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   it('Update settings', () => {
@@ -41,9 +45,11 @@ context('Access User settings', () => {
     cy.getByTestid('grw-user-settings').should('be.visible');
     cy.screenshot(`${ssPrefix}-user-information-1`);
     cy.getByTestid('grw-besic-info-settings-update-button').click();
-    cy.get('.toast').should('be.visible');
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-user-information-2`);
+
     cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+    cy.get('.toast').should('not.exist');
 
     // Access External account
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(1) a').click();
@@ -55,18 +61,22 @@ context('Access User settings', () => {
     cy.getByTestid('grw-associate-modal').find('.modal-footer button').click(); // click add button in modal form
     cy.screenshot(`${ssPrefix}-external-account-3`);
     cy.getByTestid('grw-associate-modal').find('.close').click();
-    cy.get('.toast').should('be.visible');
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-external-account-4`);
+
     cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+    cy.get('.toast').should('not.exist');
 
     // Access Password setting
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(2) a').click();
     cy.scrollTo('top');
     cy.screenshot(`${ssPrefix}-password-settings-1`);
     cy.getByTestid('grw-password-settings-update-button').click();
-    cy.get('.toast').should('be.visible');
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-password-settings-2`);
+
     cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+    cy.get('.toast').should('not.exist');
 
     // Access API setting
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(3) a').click();
@@ -74,9 +84,11 @@ context('Access User settings', () => {
     cy.screenshot(`${ssPrefix}-api-setting-1`);
     cy.getByTestid('grw-api-settings-update-button').click();
     cy.getByTestid('grw-api-settings-input').should('be.visible');
-    cy.get('.toast').should('be.visible');
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-api-setting-2`);
+
     cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+    cy.get('.toast').should('not.exist');
 
     // Access Editor setting
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(4) a').click();
@@ -84,18 +96,19 @@ context('Access User settings', () => {
     cy.getByTestid('grw-editor-settings').should('be.visible');
     cy.screenshot(`${ssPrefix}-editor-setting-1`);
     cy.getByTestid('grw-editor-settings-update-button').click();
-    cy.get('.toast').should('be.visible');
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-editor-setting-2`);
+
     cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+    cy.get('.toast').should('not.exist');
 
     // Access In-app notification setting
     cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(5) a').click();
     cy.scrollTo('top');
     cy.screenshot(`${ssPrefix}-in-app-notification-setting-1`);
     cy.getByTestid('grw-in-app-notification-settings-update-button').click();
-    cy.get('.toast').should('be.visible');
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-in-app-notification-setting-2`);
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
   });
 
 });

+ 1 - 0
packages/core/src/index.js

@@ -18,4 +18,5 @@ export * from './plugin/service/tag-cache-manager';
 export * from './models/devided-page-path';
 export * from './service/localstorage-manager';
 export * from './utils/basic-interceptor';
+export * from './utils/browser-utils';
 export * from './utils/mongoose-utils';

+ 7 - 0
packages/core/src/utils/browser-utils.ts

@@ -0,0 +1,7 @@
+export const isClient = (): boolean => {
+  return (typeof window !== 'undefined') || (typeof navigator !== 'undefined' && navigator.webdriver);
+};
+
+export const isServer = (): boolean => {
+  return !isClient();
+};