Răsfoiți Sursa

Merge pull request #5351 from weseek/imprv/vrt-tests

imprv: vrt tests
Yuki Takei 4 ani în urmă
părinte
comite
3c1d7ae962
31 a modificat fișierele cu 289 adăugiri și 69 ștergeri
  1. 2 0
      packages/app/config/ci/.env.local.for-auto-install
  2. 123 0
      packages/app/resource/search/mappings-es6-for-ci.json
  3. 1 1
      packages/app/src/client/app.jsx
  4. 2 2
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  5. 2 2
      packages/app/src/components/Admin/App/AppSettingsPageContents.jsx
  6. 2 2
      packages/app/src/components/Admin/Customize/Customize.jsx
  7. 2 2
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  8. 2 2
      packages/app/src/components/Admin/FullTextSearchManagement.jsx
  9. 2 2
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  10. 2 2
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  11. 2 2
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx
  12. 2 2
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  13. 2 2
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  14. 2 2
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  15. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  16. 2 2
      packages/app/src/components/Admin/UserManagement.jsx
  17. 1 1
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  18. 1 1
      packages/app/src/components/SearchPage/SearchResultList.tsx
  19. 7 5
      packages/app/src/components/SearchPage2/SearchPageBase.tsx
  20. 1 1
      packages/app/src/components/TrashPageList.jsx
  21. 2 1
      packages/app/src/server/crowi/index.js
  22. 11 2
      packages/app/src/server/service/config-loader.ts
  23. 27 5
      packages/app/src/server/service/installer.ts
  24. 23 5
      packages/app/src/server/service/search-delegator/elasticsearch.ts
  25. 1 1
      packages/app/src/server/views/layout-growi/page_list.html
  26. 1 1
      packages/app/src/server/views/tags.html
  27. 13 0
      packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts
  28. 5 0
      packages/app/test/cypress/integration/2-basic-features/access-to-me-page.spec.ts
  29. 0 15
      packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts
  30. 35 0
      packages/app/test/cypress/integration/2-basic-features/access-to-special-page.spec.ts
  31. 9 4
      packages/app/test/cypress/integration/3-search/access-to-result-page-directly.spec.ts

+ 2 - 0
packages/app/config/ci/.env.local.for-auto-install

@@ -5,3 +5,5 @@ AUTO_INSTALL_ADMIN_NAME=Admin
 AUTO_INSTALL_ADMIN_EMAIL=admin@example.com
 AUTO_INSTALL_ADMIN_PASSWORD=adminadmin
 AUTO_INSTALL_GLOBAL_LANG=en_US
+
+AUTO_INSTALL_SERVER_DATE=2022-01-01T00:00:00.0

+ 123 - 0
packages/app/resource/search/mappings-es6-for-ci.json

@@ -0,0 +1,123 @@
+{
+  "settings": {
+    "analysis": {
+      "filter": {
+        "english_stop": {
+          "type":       "stop",
+          "stopwords":  "_english_"
+        }
+      },
+      "tokenizer": {
+        "edge_ngram_tokenizer": {
+          "type": "edge_ngram",
+          "min_gram": 2,
+          "max_gram": 20,
+          "token_chars": ["letter", "digit"]
+        }
+      },
+      "analyzer": {
+        "japanese": {
+          "tokenizer": "edge_ngram_tokenizer",
+          "filter": [
+            "lowercase",
+            "english_stop"
+          ]
+        },
+        "english_edge_ngram": {
+          "tokenizer": "edge_ngram_tokenizer",
+          "filter": [
+            "lowercase",
+            "english_stop"
+          ]
+        }
+      }
+    }
+  },
+  "mappings": {
+    "pages": {
+      "properties" : {
+        "path": {
+          "type": "text",
+          "fields": {
+            "raw": {
+              "type": "text",
+              "analyzer": "keyword"
+            },
+            "ja": {
+              "type": "text",
+              "analyzer": "japanese"
+            },
+            "en": {
+              "type": "text",
+              "analyzer": "english_edge_ngram",
+              "search_analyzer": "standard"
+            }
+          }
+        },
+        "body": {
+          "type": "text",
+          "fields": {
+            "ja": {
+              "type": "text",
+              "analyzer": "japanese"
+            },
+            "en": {
+              "type": "text",
+              "analyzer": "english_edge_ngram",
+              "search_analyzer": "standard"
+            }
+          }
+        },
+        "comments": {
+          "type": "text",
+          "fields": {
+            "ja": {
+              "type": "text",
+              "analyzer": "japanese"
+            },
+            "en": {
+              "type": "text",
+              "analyzer": "english_edge_ngram",
+              "search_analyzer": "standard"
+            }
+          }
+        },
+        "username": {
+          "type": "keyword"
+        },
+        "comment_count": {
+          "type": "integer"
+        },
+        "bookmark_count": {
+          "type": "integer"
+        },
+        "seenUsers_count":{
+          "type": "integer"
+        },
+        "like_count": {
+          "type": "integer"
+        },
+        "grant": {
+          "type": "integer"
+        },
+        "granted_users": {
+          "type": "keyword"
+        },
+        "granted_group": {
+          "type": "keyword"
+        },
+        "created_at": {
+          "type": "date",
+          "format": "dateOptionalTime"
+        },
+        "updated_at": {
+          "type": "date",
+          "format": "dateOptionalTime"
+        },
+        "tag_names": {
+          "type": "keyword"
+        }
+      }
+    }
+  }
+}

+ 1 - 1
packages/app/src/client/app.jsx

@@ -96,7 +96,7 @@ Object.assign(componentMappings, {
 
   'trash-page-alert': <TrashPageAlert />,
 
-  'trash-page-list': <TrashPageList />,
+  'trash-page-list-container': <TrashPageList />,
 
   'not-found-page': <NotFoundPage />,
 

+ 2 - 2
packages/app/src/components/Admin/AdminHome/AdminHome.jsx

@@ -37,7 +37,7 @@ const AdminHome = (props) => {
   }, [fetchAdminHomeData]);
 
   return (
-    <>
+    <div data-testid="admin-home">
       {
       // Alert message will be displayed in case that V5 migration has not been compleated
         (migrationStatus != null && !migrationStatus.isV5Compatible)
@@ -106,7 +106,7 @@ const AdminHome = (props) => {
           </div>
         </div>
       </div>
-    </>
+    </div>
   );
 };
 

+ 2 - 2
packages/app/src/components/Admin/App/AppSettingsPageContents.jsx

@@ -19,7 +19,7 @@ class AppSettingsPageContents extends React.Component {
     const { isV5Compatible } = adminAppContainer.state;
 
     return (
-      <Fragment>
+      <div data-testid="admin-app-settings">
         {
           !isV5Compatible
           && (
@@ -66,7 +66,7 @@ class AppSettingsPageContents extends React.Component {
             <PluginSetting />
           </div>
         </div>
-      </Fragment>
+      </div>
     );
   }
 

+ 2 - 2
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -46,7 +46,7 @@ function Customize(props) {
   }
 
   return (
-    <Fragment>
+    <div data-testid="admin-customize">
       <div className="mb-5">
         <CustomizeLayoutSetting appContainer={appContainer} />
       </div>
@@ -71,7 +71,7 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeScriptSetting />
       </div>
-    </Fragment>
+    </div>
   );
 }
 

+ 2 - 2
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -210,7 +210,7 @@ class ExportArchiveDataPage extends React.Component {
     const showExportingData = (isExported || isExporting) && (progressList != null);
 
     return (
-      <Fragment>
+      <div data-testid="admin-export-archive-data">
         <h2>{t('Export Archive Data')}</h2>
 
         <button type="button" className="btn btn-outline-secondary" disabled={isExporting} onClick={this.openExportModal}>
@@ -239,7 +239,7 @@ class ExportArchiveDataPage extends React.Component {
           onClose={this.closeExportModal}
           collections={this.state.collections}
         />
-      </Fragment>
+      </div>
     );
   }
 

+ 2 - 2
packages/app/src/components/Admin/FullTextSearchManagement.jsx

@@ -14,10 +14,10 @@ class FullTextSearchManagement extends React.Component {
     const { t } = this.props;
 
     return (
-      <Fragment>
+      <div data-testid="admin-full-text-search">
         <h2> { t('full_text_search_management.elasticsearch_management') } </h2>
         <ElasticsearchManagement />
-      </Fragment>
+      </div>
     );
   }
 

+ 2 - 2
packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -13,7 +13,7 @@ class ImportDataPageContents extends React.Component {
     const { t, adminImportContainer } = this.props;
 
     return (
-      <Fragment>
+      <div data-testid="admin-import-data">
         <GrowiArchiveSection />
 
         <form
@@ -226,7 +226,7 @@ class ImportDataPageContents extends React.Component {
 
 
         </form>
-      </Fragment>
+      </div>
     );
   }
 

+ 2 - 2
packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -42,7 +42,7 @@ function LegacySlackIntegration(props) {
   const isDisabled = adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
 
   return (
-    <>
+    <div data-testid="admin-slack-integration-legacy">
       { isDisabled && (
         <div className="alert alert-danger">
           <i className="icon-minus icon-fw"></i>
@@ -58,7 +58,7 @@ function LegacySlackIntegration(props) {
       </div>
 
       <SlackConfiguration />
-    </>
+    </div>
   );
 }
 

+ 2 - 2
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx

@@ -14,7 +14,7 @@ class MarkDownSettingContents extends React.Component {
   render() {
     const { t } = this.props;
     return (
-      <React.Fragment>
+      <div data-testid="admin-markdown">
         {/* Line Break Setting */}
         <h2 className="admin-setting-header">{t('admin:markdown_setting.lineBreak_header')}</h2>
         <Card className="card well my-3">
@@ -42,7 +42,7 @@ class MarkDownSettingContents extends React.Component {
           <CardBody className="px-0 py-2">{ t('admin:markdown_setting.xss_desc') }</CardBody>
         </Card>
         <XssForm />
-      </React.Fragment>
+      </div>
     );
   }
 

+ 2 - 2
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -144,7 +144,7 @@ function NotificationSetting(props) {
   const isSlackLegacyEnabled = !isSlackbotConfigured && isSlackLegacyConfigured;
 
   return (
-    <>
+    <div data-testid="admin-notification">
       <h2 className="admin-setting-header">{t('admin:external_notification.header_status')}</h2>
       <ul className="list-group">
         { !isMounted && <SkeltonListItem />}
@@ -170,7 +170,7 @@ function NotificationSetting(props) {
           {activeComponents.has('global_notification') && <GlobalNotification />}
         </TabPane>
       </TabContent>
-    </>
+    </div>
   );
 }
 

+ 2 - 2
packages/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -81,7 +81,7 @@ function SecurityManagementContents(props) {
 
 
   return (
-    <Fragment>
+    <div data-testid="admin-security">
       <div className="mb-5">
         <SecuritySetting />
       </div>
@@ -141,7 +141,7 @@ function SecurityManagementContents(props) {
           </TabPane>
         </TabContent>
       </div>
-    </Fragment>
+    </div>
   );
 
 }

+ 2 - 2
packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -192,7 +192,7 @@ const SlackIntegration = (props) => {
   }
 
   return (
-    <>
+    <div data-testid="admin-slack-integration">
       <ConfirmBotChangeModal
         isOpen={selectedBotType != null}
         onConfirmClick={changeCurrentBotSettingsHandler}
@@ -246,7 +246,7 @@ const SlackIntegration = (props) => {
       </div>
 
       {settingsComponent}
-    </>
+    </div>
   );
 };
 

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -111,7 +111,7 @@ const UserGroupPage: FC<Props> = (props: Props) => {
   }, [mutateUserGroups]);
 
   return (
-    <Fragment>
+    <div data-testid="admin-user-groups">
       {
         isAclEnabled ? (
           <div className="mb-2">
@@ -147,7 +147,7 @@ const UserGroupPage: FC<Props> = (props: Props) => {
           onHide={hideDeleteModal}
         />
       </>
-    </Fragment>
+    </div>
   );
 };
 

+ 2 - 2
packages/app/src/components/Admin/UserManagement.jsx

@@ -141,7 +141,7 @@ class UserManagement extends React.Component {
     );
 
     return (
-      <Fragment>
+      <div data-testid="admin-users">
         {adminUsersContainer.state.userForPasswordResetModal != null
         && (
           <PasswordResetModal
@@ -212,7 +212,7 @@ class UserManagement extends React.Component {
         <UserTable />
         {pager}
 
-      </Fragment>
+      </div>
     );
   }
 

+ 1 - 1
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -153,7 +153,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   if (page == null) return <></>;
 
   return (
-    <div key={page._id} className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
+    <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
       <div className="grw-subnav-append-shadow-container">
         <GrowiSubNavigation
           page={page}

+ 1 - 1
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -79,7 +79,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
   }
 
   return (
-    <ul className="page-list-ul list-group list-group-flush">
+    <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">
       { (injectedPage ?? pages).map((page, i) => {
         return (
           <PageListItemL

+ 7 - 5
packages/app/src/components/SearchPage2/SearchPageBase.tsx

@@ -106,11 +106,9 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll, Props> =
     }
   }, [onSelectedPagesByCheckboxesChanged, pages, selectedPageIdsByCheckboxes]);
 
-  const isLoading = pages == null;
-
   return (
     <div className="content-main">
-      <div className="search-result-base d-flex">
+      <div className="search-result-base d-flex" data-testid="search-result-base">
 
         <div className="mw-0 flex-grow-1 flex-basis-0 border boder-gray search-result-list" id="search-result-list">
 
@@ -118,24 +116,28 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll, Props> =
 
           <div className="search-result-list-scroll">
 
-            { isLoading && (
+            {/* Loading */}
+            { pages == null && (
               <div className="mw-0 flex-grow-1 flex-basis-0 m-5 text-muted text-center">
                 <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
               </div>
             ) }
 
-            { !isLoading && (
+            {/* Loaded */}
+            { pages != null && (
               <>
                 <div className="my-3 px-md-4 px-3">
                   {searchResultListHead}
                 </div>
 
+                {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
                 { pages.length === 0 && (
                   <div className="d-flex justify-content-center h2 text-muted my-5">
                     0 {t('search_result.page_number_unit')}
                   </div>
                 ) }
 
+                {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
                 { pages.length > 0 && (
                   <div className="page-list px-md-4">
                     <SearchResultList

+ 1 - 1
packages/app/src/components/TrashPageList.jsx

@@ -21,7 +21,7 @@ const TrashPageList = (props) => {
   }, [t]);
 
   return (
-    <div className="mt-5 d-edit-none">
+    <div data-testid="trash-page-list" className="mt-5 d-edit-none">
       <CustomNavAndContents navTabMapping={navTabMapping} />
     </div>
   );

+ 2 - 1
packages/app/src/server/crowi/index.js

@@ -398,11 +398,12 @@ Crowi.prototype.autoInstall = function() {
     admin: true,
   };
   const globalLang = this.configManager.getConfig('crowi', 'autoInstall:globalLang');
+  const serverDate = this.configManager.getConfig('crowi', 'autoInstall:serverDate');
 
   const installerService = new InstallerService(this);
 
   try {
-    installerService.install(firstAdminUserToSave, globalLang ?? 'en_US');
+    installerService.install(firstAdminUserToSave, globalLang ?? 'en_US', serverDate);
   }
   catch (err) {
     logger.warn('Automatic installation failed.', err);

+ 11 - 2
packages/app/src/server/service/config-loader.ts

@@ -1,3 +1,5 @@
+import { parseISO } from 'date-fns';
+
 import { envUtils } from '@growi/core';
 
 import loggerFactory from '~/utils/logger';
@@ -9,7 +11,7 @@ import ConfigModel, {
 
 const logger = loggerFactory('growi:service:ConfigLoader');
 
-enum ValueType { NUMBER, STRING, BOOLEAN }
+enum ValueType { NUMBER, STRING, BOOLEAN, DATE }
 
 interface ValueParser<T> {
   parse(value: string): T;
@@ -26,10 +28,11 @@ type EnumDictionary<T extends string | symbol | number, U> = {
   [K in T]: U;
 };
 
-const parserDictionary: EnumDictionary<ValueType, ValueParser<number | string | boolean>> = {
+const parserDictionary: EnumDictionary<ValueType, ValueParser<number | string | boolean | Date>> = {
   [ValueType.NUMBER]:  { parse: (v: string) => { return parseInt(v, 10) } },
   [ValueType.STRING]:  { parse: (v: string) => { return v } },
   [ValueType.BOOLEAN]: { parse: (v: string) => { return envUtils.toBoolean(v) } },
+  [ValueType.DATE]:    { parse: (v: string) => { return parseISO(v) } },
 };
 
 /**
@@ -208,6 +211,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.STRING,
     default: null,
   },
+  AUTO_INSTALL_SERVER_DATE: {
+    ns:      'crowi',
+    key:     'autoInstall:serverDate',
+    type:    ValueType.DATE,
+    default: null,
+  },
   S2SMSG_PUBSUB_SERVER_TYPE: {
     ns:      'crowi',
     key:     's2sMessagingPubsub:serverType',

+ 27 - 5
packages/app/src/server/service/installer.ts

@@ -34,7 +34,12 @@ export class InstallerService {
       return;
     }
 
-    await searchService.rebuildIndex();
+    try {
+      await searchService.rebuildIndex();
+    }
+    catch (err) {
+      logger.error('Rebuild index failed', err);
+    }
   }
 
   private async createPage(filePath, pagePath, owner): Promise<IPage|undefined> {
@@ -53,7 +58,7 @@ export class InstallerService {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  private async createInitialPages(owner, lang: Lang): Promise<any> {
+  private async createInitialPages(owner, lang: Lang, initialPagesCreatedAt?: Date): Promise<any> {
     const { localeDir } = this.crowi;
     // create /Sandbox/*
     /*
@@ -66,6 +71,19 @@ export class InstallerService {
       this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner),
     ]);
 
+    // update createdAt and updatedAt fields of all pages
+    if (initialPagesCreatedAt != null) {
+      try {
+        // TODO typescriptize models/user.js and remove eslint-disable-next-line
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const Page = mongoose.model('Page') as any;
+        await Page.updateMany({}, { createdAt: initialPagesCreatedAt, updatedAt: initialPagesCreatedAt });
+      }
+      catch (err) {
+        logger.error('Failed to update createdAt', err);
+      }
+    }
+
     try {
       await this.initSearchIndex();
     }
@@ -85,7 +103,7 @@ export class InstallerService {
     return configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
   }
 
-  async install(firstAdminUserToSave: IUser, globalLang: Lang): Promise<IUser> {
+  async install(firstAdminUserToSave: IUser, globalLang: Lang, initialPagesCreatedAt?: Date): Promise<IUser> {
     await this.initDB(globalLang);
 
     // TODO typescriptize models/user.js and remove eslint-disable-next-line
@@ -94,7 +112,11 @@ export class InstallerService {
     const Page = mongoose.model('Page') as any;
 
     // create portal page for '/' before creating admin user
-    await this.createPage(path.join(this.crowi.localeDir, globalLang, 'welcome.md'), '/', { _id: '000000000000000000000000' }); // use 0 as a mock user id
+    await this.createPage(
+      path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
+      '/',
+      { _id: '000000000000000000000000' }, // use 0 as a mock user id
+    );
 
     // create first admin user
     // TODO: with transaction
@@ -120,7 +142,7 @@ export class InstallerService {
     await Promise.all([rootPage.save(), rootRevision.save()]);
 
     // create initial pages
-    await this.createInitialPages(adminUser, globalLang);
+    await this.createInitialPages(adminUser, globalLang, initialPagesCreatedAt);
 
     return adminUser;
   }

+ 23 - 5
packages/app/src/server/service/search-delegator/elasticsearch.ts

@@ -149,10 +149,16 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
     };
   }
 
-  async init() {
+  async init(): Promise<void> {
     const normalizeIndices = await this.normalizeIndices();
     if (this.isElasticsearchReindexOnBoot) {
-      return this.rebuildIndex();
+      try {
+        await this.rebuildIndex();
+      }
+      catch (err) {
+        logger.error('Rebuild index on boot failed', err);
+      }
+      return;
     }
     return normalizeIndices;
   }
@@ -284,7 +290,8 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
       await this.addAllPages();
     }
     catch (error) {
-      logger.warn('An error occured while \'rebuildIndex\', normalize indices anyway.');
+      logger.error('An error occured while \'rebuildIndex\'.', error);
+      logger.error('error.meta.body', error?.meta?.body);
 
       const socket = this.socketIoService.getAdminSocket();
       socket.emit('rebuildingFailed', { error: error.message });
@@ -292,6 +299,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
       throw error;
     }
     finally {
+      logger.warn('Normalize indices anyway.');
       await this.normalizeIndices();
     }
 
@@ -325,8 +333,18 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
   }
 
   async createIndex(index) {
-    const body = this.isElasticsearchV6 ? require('^/resource/search/mappings-es6.json') : require('^/resource/search/mappings-es7.json');
-    return this.client.indices.create({ index, body });
+    let mappings = this.isElasticsearchV6
+      ? require('^/resource/search/mappings-es6.json')
+      : require('^/resource/search/mappings-es7.json');
+
+    if (process.env.CI) {
+      mappings = require('^/resource/search/mappings-es6-for-ci.json');
+    }
+
+    return this.client.indices.create({
+      index,
+      body: mappings,
+    });
   }
 
   /**

+ 1 - 1
packages/app/src/server/views/layout-growi/page_list.html

@@ -15,7 +15,7 @@
 {% block content_main_after %}
   {% if isTrashPage(path) %}
     <div class="grw-container-convertible">
-      <div id="trash-page-list"></div>
+      <div id="trash-page-list-container"></div>
     </div>
   {% endif %}
 {% endblock %}

+ 1 - 1
packages/app/src/server/views/tags.html

@@ -9,7 +9,7 @@
 <div class="grw-container-convertible">
   <div class="row">
     <div id="main" class="main mt-3 col-md-12 tags-page">
-      <div class="" id="tags-page"></div>
+      <div class="" id="tags-page" data-testid="tags-page"></div>
     </div>
   </div>
 </div><!-- /.container-fluid -->

+ 13 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts

@@ -37,66 +37,79 @@ context('Access to Admin page', () => {
 
   it('/admin is successfully loaded', () => {
     cy.visit('/admin');
+    cy.getByTestid('admin-home').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin`, { capture: 'viewport' });
   });
 
   it('/admin/app is successfully loaded', () => {
     cy.visit('/admin/app');
+    cy.getByTestid('admin-app-settings').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-app`, { capture: 'viewport' });
   });
 
   it('/admin/security is successfully loaded', () => {
     cy.visit('/admin/security');
+    cy.getByTestid('admin-security').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-security`, { capture: 'viewport' });
   });
 
   it('/admin/markdown is successfully loaded', () => {
     cy.visit('/admin/markdown');
+    cy.getByTestid('admin-markdown').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-markdown`, { capture: 'viewport' });
   });
 
   it('/admin/customize is successfully loaded', () => {
     cy.visit('/admin/customize');
+    cy.getByTestid('admin-customize').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-customize`, { capture: 'viewport' });
   });
 
   it('/admin/importer is successfully loaded', () => {
     cy.visit('/admin/importer');
+    cy.getByTestid('admin-import-data').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-importer`, { capture: 'viewport' });
   });
 
   it('/admin/export is successfully loaded', () => {
     cy.visit('/admin/export');
+    cy.getByTestid('admin-export-archive-data').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-export`, { capture: 'viewport' });
   });
 
   it('/admin/notification is successfully loaded', () => {
     cy.visit('/admin/notification');
+    cy.getByTestid('admin-notification').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-notification`, { capture: 'viewport' });
   });
 
   it('/admin/slack-integration is successfully loaded', () => {
     cy.visit('/admin/slack-integration');
+    cy.getByTestid('admin-slack-integration').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-slack-integration`, { capture: 'viewport' });
   });
 
   it('/admin/slack-integration-legacy is successfully loaded', () => {
     cy.visit('/admin/slack-integration-legacy');
+    cy.getByTestid('admin-slack-integration-legacy').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-slack-integration-legacy`, { capture: 'viewport' });
   });
 
   it('/admin/users is successfully loaded', () => {
     cy.visit('/admin/users');
+    cy.getByTestid('admin-users').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-users`, { capture: 'viewport' });
   });
 
   it('/admin/user-groups is successfully loaded', () => {
     cy.visit('/admin/user-groups');
+    cy.getByTestid('admin-user-groups').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-user-groups`, { capture: 'viewport' });
   });
 
   it('/admin/search is successfully loaded', () => {
     cy.visit('/admin/search');
+    cy.getByTestid('admin-full-text-search').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-search`, { capture: 'viewport' });
   });
 

+ 5 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-me-page.spec.ts

@@ -25,4 +25,9 @@ context('Access to page', () => {
     cy.screenshot(`${ssPrefix}-me`, { capture: 'viewport' });
   });
 
+  it('Draft page is successfully shown', () => {
+    cy.visit('/me/drafts');
+    cy.screenshot(`${ssPrefix}-draft-page`, { capture: 'viewport' });
+  });
+
 });

+ 0 - 15
packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts

@@ -30,11 +30,6 @@ context('Access to page', () => {
     cy.screenshot(`${ssPrefix}-sandbox-headers`, { capture: 'viewport' });
   });
 
-  it('/trash is successfully loaded', () => {
-    cy.visit('/trash', {  });
-    cy.screenshot(`${ssPrefix}-trash`, { capture: 'viewport' });
-  });
-
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
     cy.screenshot(`${ssPrefix}-sandbox-math`, { capture: 'viewport' });
@@ -50,14 +45,4 @@ context('Access to page', () => {
     cy.screenshot(`${ssPrefix}-user-admin`, { capture: 'viewport' });
   });
 
-  it('Draft page is successfully shown', () => {
-    cy.visit('/me/drafts');
-    cy.screenshot(`${ssPrefix}-draft-page`, { capture: 'viewport' });
-  });
-  
-  it('/tags is successfully loaded', () => {
-    cy.visit('/tags');
-    cy.screenshot(`${ssPrefix}-tags`, { capture: 'viewport' });
-  });
-  
 });

+ 35 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-special-page.spec.ts

@@ -0,0 +1,35 @@
+
+context('Access to special pages', () => {
+  const ssPrefix = 'access-to-special-pages-';
+
+  let connectSid: string | undefined;
+
+  before(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    cy.getCookie('connect.sid').then(cookie => {
+      connectSid = cookie?.value;
+    });
+  });
+
+  beforeEach(() => {
+    if (connectSid != null) {
+      cy.setCookie('connect.sid', connectSid);
+    }
+  });
+
+  it('/trash is successfully loaded', () => {
+    cy.visit('/trash', {  });
+    cy.getByTestid('trash-page-list').should('be.visible');
+    cy.screenshot(`${ssPrefix}-trash`, { capture: 'viewport' });
+  });
+
+  it('/tags is successfully loaded', () => {
+    cy.visit('/tags');
+    cy.getByTestid('tags-page').should('be.visible');
+    cy.screenshot(`${ssPrefix}-tags`, { capture: 'viewport' });
+  });
+
+});

+ 9 - 4
packages/app/test/cypress/integration/3-search/access-to-result-page-directly.spec.ts

@@ -20,14 +20,19 @@ context('Access to search result page directly', () => {
   });
 
   it('/_search with "q" param is successfully loaded', () => {
-    cy.visit('/_search', { qs: { q: 'sandbox headers blockquotes' } });
-    // eslint-disable-next-line cypress/no-unnecessary-waiting
-    cy.wait(1000);
+    cy.visit('/_search', { qs: { q: 'bootstrap4' } });
+
+    cy.getByTestid('search-result-list').should('be.visible');
+    cy.getByTestid('search-result-content').should('be.visible');
+
     cy.screenshot(`${ssPrefix}-with-q`, { capture: 'viewport' });
   });
 
   it('checkboxes behaviors', () => {
-    cy.visit('/_search', { qs: { q: 'sandbox headers blockquotes' } });
+    cy.visit('/_search', { qs: { q: 'bootstrap4' } });
+
+    cy.getByTestid('search-result-base').should('be.visible');
+    cy.getByTestid('search-result-list').should('be.visible');
 
     cy.getByTestid('cb-select').first().click({force: true});
     cy.screenshot(`${ssPrefix}-the-first-checkbox-on`, { capture: 'viewport' });