Przeglądaj źródła

Merge branch 'feat/unit-test-for-getparentandfillancestors' of https://github.com/weseek/growi into feat/unit-test-for-getparentandfillancestors

yohei0125 4 lat temu
rodzic
commit
d9ae436db7
26 zmienionych plików z 265 dodań i 225 usunięć
  1. 7 0
      .devcontainer/Dockerfile
  2. 9 14
      .github/workflows/reusable-app-prod.yml
  3. 2 2
      packages/app/cypress.json
  4. 1 1
      packages/app/package.json
  5. 7 7
      packages/app/src/components/Fab.jsx
  6. 7 3
      packages/app/src/components/Me/ApiSettings.jsx
  7. 5 4
      packages/app/src/components/Me/AssociateModal.jsx
  8. 10 3
      packages/app/src/components/Me/BasicInfoSettings.jsx
  9. 11 4
      packages/app/src/components/Me/EditorSettings.tsx
  10. 12 4
      packages/app/src/components/Me/ExternalAccountLinkedMe.jsx
  11. 4 2
      packages/app/src/components/Me/InAppNotificationSettings.tsx
  12. 5 3
      packages/app/src/components/Me/PasswordSettings.jsx
  13. 8 4
      packages/app/src/components/Me/PersonalSettings.jsx
  14. 4 3
      packages/app/src/components/Me/UserSettings.jsx
  15. 7 8
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  16. 7 10
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  17. 25 0
      packages/app/src/migrations/20220411114257-set-sparse-option-to-slack-member-id.js
  18. 5 4
      packages/app/src/server/models/obsolete-page.js
  19. 13 25
      packages/app/src/server/service/search-delegator/elasticsearch.ts
  20. 1 1
      packages/app/src/server/views/layout-growi/user_page.html
  21. 1 1
      packages/app/src/server/views/layout/layout.html
  22. 2 5
      packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts
  23. 0 32
      packages/app/test/cypress/integration/6-home-settings/access-home.spec.ts
  24. 0 85
      packages/app/test/cypress/integration/6-home-settings/access-user-settings.spec.ts
  25. 101 0
      packages/app/test/cypress/integration/6-home/home.spec.ts
  26. 11 0
      packages/app/test/cypress/plugins/index.ts

+ 7 - 0
.devcontainer/Dockerfile

@@ -30,8 +30,15 @@ RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
 # * See https://docs.docker.com/engine/reference/builder/#run *
 # *************************************************************
 ENV DEBIAN_FRONTEND=noninteractive
+
+# Prepare to install Chrome for VRT
+RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
+RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
+
 RUN apt-get update \
    && apt-get -y install --no-install-recommends git-lfs \
+      # Chrome
+      google-chrome-stable \
       # for Cypress
       libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb fonts-noto-cjk \
 

+ 9 - 14
.github/workflows/reusable-app-prod.yml

@@ -175,17 +175,12 @@ jobs:
     if: ${{ !inputs.skip-cypress }}
 
     runs-on: ubuntu-latest
-    container:
-      image: cypress/base:16.13.0
-      # solve permissions issue
-      # see: https://github.com/cypress-io/github-action/issues/446#issuecomment-987015822
-      options: --user 1001
 
     strategy:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['1', '2', '3', '4']
+        spec-group: ['1', '2', '3', '4', '6']
 
     services:
       mongodb:
@@ -202,10 +197,11 @@ jobs:
     steps:
     - uses: actions/checkout@v3
 
-    - name: Get yarn cache dir
-      id: yarn-cache-dir
-      run: |
-        echo "::set-output name=value::`yarn cache dir --silent`"
+    - uses: actions/setup-node@v3
+      with:
+        node-version: ${{ matrix.node-version }}
+        cache: 'yarn'
+        cache-dependency-path: '**/yarn.lock'
 
     - name: Cache/Restore dependencies
       uses: actions/cache@v3
@@ -213,7 +209,6 @@ jobs:
         path: |
           **/node_modules
           ~/.cache/Cypress
-          ${{ steps.yarn-cache-dir.outputs.value }}
         key: deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
         restore-keys: |
           deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-
@@ -252,14 +247,14 @@ jobs:
     - name: Cypress Run
       uses: cypress-io/github-action@v3
       with:
+        browser: chrome
         working-directory: ./packages/app
-        install: false
         spec: '${{ steps.determine-spec-exp.outputs.value }}'
         start: yarn server
         wait-on: 'http://localhost:3000'
       env:
-        MONGO_URI: mongodb://mongodb:27017/growi-vrt
-        ELASTICSEARCH_URI: http://elasticsearch:9200/growi
+        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-vrt
+        ELASTICSEARCH_URI: http://localhost:${{ job.services.elasticsearch.ports['9200'] }}/growi
 
     - name: Upload results
       if: always()

+ 2 - 2
packages/app/cypress.json

@@ -10,8 +10,8 @@
   "pluginsFile": "test/cypress/plugins/index.ts",
   "testFiles": "**/*.spec.ts",
 
-  "viewportWidth": 1440,
-  "viewportHeight": 1200,
+  "viewportWidth": 1400,
+  "viewportHeight": 1024,
 
   "experimentalSessionSupport": true
 }

+ 1 - 1
packages/app/package.json

@@ -28,7 +28,7 @@
     "dev:migrate:status": "yarn dev:migrate-mongo status",
     "dev:migrate:up": "yarn dev:migrate-mongo up",
     "dev:migrate:down": "yarn dev:migrate-mongo down",
-    "cy:run": "cypress run --headless",
+    "cy:run": "cypress run --browser chrome",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",

+ 7 - 7
packages/app/src/components/Fab.jsx

@@ -1,18 +1,18 @@
 import React, { useState, useCallback, useEffect } from 'react';
+
 import PropTypes from 'prop-types';
 import StickyEvents from 'sticky-events';
-import loggerFactory from '~/utils/logger';
 
 
 import AppContainer from '~/client/services/AppContainer';
-
-import { usePageCreateModal } from '~/stores/modal';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
+import { useCurrentPagePath } from '~/stores/context';
+import { usePageCreateModal } from '~/stores/modal';
+import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from './UnstatedUtils';
 import CreatePageIcon from './Icons/CreatePageIcon';
 import ReturnTopIcon from './Icons/ReturnTopIcon';
-import { useCurrentPagePath } from '~/stores/context';
+import { withUnstatedContainers } from './UnstatedUtils';
 
 const logger = loggerFactory('growi:cli:Fab');
 
@@ -55,7 +55,7 @@ const Fab = (props) => {
   function renderPageCreateButton() {
     return (
       <>
-        <div data-testid="grw-fab-create-page" className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
+        <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
           <button
             type="button"
             className={`btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light ${buttonClasses}`}
@@ -69,7 +69,7 @@ const Fab = (props) => {
   }
 
   return (
-    <div className="grw-fab d-none d-md-block d-edit-none">
+    <div className="grw-fab d-none d-md-block d-edit-none" data-testid="grw-fab">
       {currentUser != null && renderPageCreateButton()}
       <div data-testid="grw-fab-return-to-top" className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button

+ 7 - 3
packages/app/src/components/Me/ApiSettings.jsx

@@ -1,13 +1,14 @@
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 class ApiSettings extends React.Component {
@@ -46,6 +47,8 @@ class ApiSettings extends React.Component {
             {personalContainer.state.apiToken != null
               ? (
                 <input
+                  data-testid="grw-api-settings-input"
+                  data-hide-in-vrt
                   className="form-control"
                   type="text"
                   name="apiToken"
@@ -76,6 +79,7 @@ class ApiSettings extends React.Component {
         <div className="row my-3">
           <div className="offset-4 col-5">
             <button
+              data-testid="grw-api-settings-update-button"
               type="button"
               className="btn btn-primary text-nowrap"
               onClick={this.onClickSubmit}

+ 5 - 4
packages/app/src/components/Me/AssociateModal.jsx

@@ -1,21 +1,22 @@
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-
 import {
   Modal,
   ModalHeader,
   ModalBody,
   ModalFooter,
 } from 'reactstrap';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { withUnstatedContainers } from '../UnstatedUtils';
+
 
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import LdapAuthTest from '../Admin/Security/LdapAuthTest';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 class AssociateModal extends React.Component {
 
@@ -70,7 +71,7 @@ class AssociateModal extends React.Component {
     const { t } = this.props;
 
     return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg" data-testid="grw-associate-modal">
         <ModalHeader className="bg-primary text-light" toggle={this.props.onClose}>
           { t('admin:user_management.create_external_account') }
         </ModalHeader>

+ 10 - 3
packages/app/src/components/Me/BasicInfoSettings.jsx

@@ -1,14 +1,15 @@
 
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import PersonalContainer from '~/client/services/PersonalContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { localeMetadatas } from '~/client/util/i18n';
 
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
-import PersonalContainer from '~/client/services/PersonalContainer';
 
 class BasicInfoSettings extends React.Component {
 
@@ -144,7 +145,13 @@ class BasicInfoSettings extends React.Component {
 
         <div className="row my-3">
           <div className="offset-4 col-5">
-            <button type="button" className="btn btn-primary" onClick={this.onClickSubmit} disabled={personalContainer.state.retrieveError != null}>
+            <button
+              data-testid="grw-besic-info-settings-update-button"
+              type="button"
+              className="btn btn-primary"
+              onClick={this.onClickSubmit}
+              disabled={personalContainer.state.retrieveError != null}
+            >
               {t('Update')}
             </button>
           </div>

+ 11 - 4
packages/app/src/components/Me/EditorSettings.tsx

@@ -2,13 +2,15 @@ import React, {
   Dispatch,
   FC, SetStateAction, useCallback, useEffect, useState,
 } from 'react';
-import { useTranslation } from 'react-i18next';
+
 import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
 import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 type EditorSettingsBodyProps = {
   appContainer: AppContainer
@@ -249,8 +251,12 @@ const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
     }
   };
 
+  if (textlintRules == null) {
+    return <></>;
+  }
+
   return (
-    <>
+    <div data-testid="grw-editor-settings">
       <RuleListGroup
         title="editor_settings.common_settings.common_settings"
         ruleList={commonRulesMenuItems}
@@ -267,6 +273,7 @@ const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
       <div className="row my-3">
         <div className="offset-4 col-5">
           <button
+            data-testid="grw-editor-settings-update-button"
             type="button"
             className="btn btn-primary"
             onClick={updateRulesHandler}
@@ -275,7 +282,7 @@ const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
           </button>
         </div>
       </div>
-    </>
+    </div>
   );
 };
 

+ 12 - 4
packages/app/src/components/Me/ExternalAccountLinkedMe.jsx

@@ -1,16 +1,19 @@
 
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
 
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
-import ExternalAccountRow from './ExternalAccountRow';
+import { toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+
 import AssociateModal from './AssociateModal';
 import DisassociateModal from './DisassociateModal';
+import ExternalAccountRow from './ExternalAccountRow';
 
 class ExternalAccountLinkedMe extends React.Component {
 
@@ -68,7 +71,12 @@ class ExternalAccountLinkedMe extends React.Component {
     return (
       <Fragment>
         <h2 className="border-bottom my-4">
-          <button type="button" className="btn btn-outline-secondary btn-sm pull-right" onClick={this.openAssociateModal}>
+          <button
+            type="button"
+            data-testid="grw-external-account-add-button"
+            className="btn btn-outline-secondary btn-sm pull-right"
+            onClick={this.openAssociateModal}
+          >
             <i className="icon-plus" aria-hidden="true" />
             Add
           </button>

+ 4 - 2
packages/app/src/components/Me/InAppNotificationSettings.tsx

@@ -2,10 +2,11 @@ import React, {
   FC, useState, useEffect, useCallback,
 } from 'react';
 
-import { useTranslation } from 'react-i18next';
 import { pullAllBy } from 'lodash';
-import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import { useTranslation } from 'react-i18next';
+
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { subscribeRuleNames, SubscribeRuleDescriptions } from '~/interfaces/in-app-notification';
 
 type SubscribeRule = {
@@ -96,6 +97,7 @@ const InAppNotificationSettings: FC = () => {
       <div className="row my-3">
         <div className="offset-4 col-5">
           <button
+            data-testid="grw-in-app-notification-settings-update-button"
             type="button"
             className="btn btn-primary"
             onClick={updateSettingsHandler}

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

@@ -1,13 +1,14 @@
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 class PasswordSettings extends React.Component {
@@ -138,6 +139,7 @@ class PasswordSettings extends React.Component {
         <div className="row my-3">
           <div className="offset-5">
             <button
+              data-testid="grw-password-settings-update-button"
               type="button"
               className="btn btn-primary"
               onClick={this.onClickSubmit}

+ 8 - 4
packages/app/src/components/Me/PersonalSettings.jsx

@@ -1,15 +1,17 @@
 
 import React, { useMemo } from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
-import UserSettings from './UserSettings';
-import PasswordSettings from './PasswordSettings';
-import ExternalAccountLinkedMe from './ExternalAccountLinkedMe';
+
 import ApiSettings from './ApiSettings';
 import { EditorSettings } from './EditorSettings';
+import ExternalAccountLinkedMe from './ExternalAccountLinkedMe';
 import InAppNotificationSettings from './InAppNotificationSettings';
+import PasswordSettings from './PasswordSettings';
+import UserSettings from './UserSettings';
 
 const PersonalSettings = (props) => {
 
@@ -58,7 +60,9 @@ const PersonalSettings = (props) => {
 
 
   return (
-    <CustomNavAndContents navTabMapping={navTabMapping} navigationMode="both" tabContentClasses={['px-0']} />
+    <div data-testid="grw-personal-settings">
+      <CustomNavAndContents navTabMapping={navTabMapping} navigationMode="both" tabContentClasses={['px-0']} />
+    </div>
   );
 
 };

+ 4 - 3
packages/app/src/components/Me/UserSettings.jsx

@@ -1,5 +1,6 @@
 
-import React, { Fragment } from 'react';
+import React from 'react';
+
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
@@ -12,7 +13,7 @@ class UserSettings extends React.Component {
     const { t } = this.props;
 
     return (
-      <Fragment>
+      <div data-testid="grw-user-settings">
         <div className="mb-5">
           <h2 className="border-bottom my-4">{t('Basic Info')}</h2>
           <BasicInfoSettings />
@@ -21,7 +22,7 @@ class UserSettings extends React.Component {
           <h2 className="border-bottom my-4">{t('Set Profile Image')}</h2>
           <ProfileImageSettings />
         </div>
-      </Fragment>
+      </div>
     );
   }
 

+ 7 - 8
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -1,22 +1,21 @@
 import React, { FC, memo } from 'react';
-import PropTypes from 'prop-types';
 
+import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
-
 import { UncontrolledTooltip } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
 import { IUser } from '~/interfaces/user';
-import { useIsDeviceSmallerThanMd } from '~/stores/ui';
-import { usePageCreateModal } from '~/stores/modal';
 import { useIsSearchPage, useCurrentPagePath } from '~/stores/context';
+import { usePageCreateModal } from '~/stores/modal';
+import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
 import GrowiLogo from '../Icons/GrowiLogo';
+import InAppNotificationDropdown from '../InAppNotification/InAppNotificationDropdown';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
-import PersonalDropdown from './PersonalDropdown';
 import GlobalSearch from './GlobalSearch';
-import InAppNotificationDropdown from '../InAppNotification/InAppNotificationDropdown';
+import PersonalDropdown from './PersonalDropdown';
 
 
 type NavbarRightProps = {
@@ -52,7 +51,7 @@ const NavbarRight: FC<NavbarRightProps> = memo((props: NavbarRightProps) => {
         </button>
       </li>
 
-      <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret">
+      <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
         <PersonalDropdown />
       </li>
     </>

+ 7 - 10
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -1,18 +1,13 @@
 import React, { useState, useCallback } from 'react';
-import PropTypes from 'prop-types';
 
+import { UserPicture } from '@growi/ui';
+import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-
 import { UncontrolledTooltip } from 'reactstrap';
 
-import { UserPicture } from '@growi/ui';
 
-import { useUserUISettings } from '~/client/services/user-ui-settings';
 import AppContainer from '~/client/services/AppContainer';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
-
+import { useUserUISettings } from '~/client/services/user-ui-settings';
 import {
   isUserPreferenceExists,
   isDarkMode as isDarkModeByUtil,
@@ -21,12 +16,14 @@ import {
   updateUserPreference,
   updateUserPreferenceWithOsSettings,
 } from '~/client/util/color-scheme';
+import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
 
 
-import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
-import SidebarDockIcon from '../Icons/SidebarDockIcon';
 import MoonIcon from '../Icons/MoonIcon';
+import SidebarDockIcon from '../Icons/SidebarDockIcon';
+import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SunIcon from '../Icons/SunIcon';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 const PersonalDropdown = (props) => {

+ 25 - 0
packages/app/src/migrations/20220411114257-set-sparse-option-to-slack-member-id.js

@@ -0,0 +1,25 @@
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
+import mongoose from 'mongoose';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:migrate:set-sparse-option-to-slack-member-id');
+
+/**
+ * set sparse option to slackMemberId
+ */
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    const User = getModelSafely('User') || require('~/server/models/user')();
+    await User.syncIndexes();
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 5 - 4
packages/app/src/server/models/obsolete-page.js

@@ -9,12 +9,13 @@ import loggerFactory from '~/utils/logger';
 /* eslint-disable no-use-before-define */
 
 const debug = require('debug')('growi:models:page');
+
 const nodePath = require('path');
-const urljoin = require('url-join');
-const mongoose = require('mongoose');
-const differenceInYears = require('date-fns/differenceInYears');
 
+const differenceInYears = require('date-fns/differenceInYears');
 const escapeStringRegexp = require('escape-string-regexp');
+const mongoose = require('mongoose');
+const urljoin = require('url-join');
 
 const { isTopPage, isTrashPage } = pagePathUtils;
 const { checkTemplatePath } = templateChecker;
@@ -253,7 +254,7 @@ export const getPageSchema = (crowi) => {
 
     this.grant = grant || GRANT_PUBLIC;
 
-    if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP) {
+    if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP && grant !== GRANT_RESTRICTED) {
       this.grantedUsers.push(user._id);
     }
 

+ 13 - 25
packages/app/src/server/service/search-delegator/elasticsearch.ts

@@ -1,22 +1,24 @@
+import { Writable, Transform } from 'stream';
+import { URL } from 'url';
+
 import elasticsearch6 from '@elastic/elasticsearch6';
 import elasticsearch7 from '@elastic/elasticsearch7';
 import mongoose from 'mongoose';
-
-import { URL } from 'url';
-
-import { Writable, Transform } from 'stream';
 import streamToPromise from 'stream-to-promise';
 
-import { createBatchStream } from '../../util/batch-stream';
-import loggerFactory from '~/utils/logger';
-import { PageModel } from '../../models/page';
-import {
-  SearchDelegator, SearchableData, QueryTerms,
-} from '../../interfaces/search';
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import {
   IFormattedSearchResult, ISearchResult, SORT_AXIS, SORT_ORDER,
 } from '~/interfaces/search';
+import loggerFactory from '~/utils/logger';
+
+import {
+  SearchDelegator, SearchableData, QueryTerms,
+} from '../../interfaces/search';
+import { PageModel } from '../../models/page';
+import { createBatchStream } from '../../util/batch-stream';
+
+
 import ElasticsearchClient from './elasticsearch-client';
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
@@ -855,27 +857,13 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
 
     const Page = mongoose.model('Page') as unknown as PageModel;
     const {
-      GRANT_PUBLIC, GRANT_RESTRICTED, GRANT_SPECIFIED, GRANT_OWNER, GRANT_USER_GROUP,
+      GRANT_PUBLIC, GRANT_SPECIFIED, GRANT_OWNER, GRANT_USER_GROUP,
     } = Page;
 
     const grantConditions: any[] = [
       { term: { grant: GRANT_PUBLIC } },
     ];
 
-    // ensure to hit to GRANT_RESTRICTED pages that the user specified at own
-    if (user != null) {
-      grantConditions.push(
-        {
-          bool: {
-            must: [
-              { term: { grant: GRANT_RESTRICTED } },
-              { term: { granted_users: user._id.toString() } },
-            ],
-          },
-        },
-      );
-    }
-
     if (showPagesRestrictedByOwner) {
       grantConditions.push(
         { term: { grant: GRANT_SPECIFIED } },

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

@@ -1,7 +1,7 @@
 {% extends 'page.html' %}
 
 {% block content_main %}
-  <div class="grw-container-convertible user-page">
+  <div class="grw-container-convertible user-page" data-testid="grw-user-page">
 
     {% include '../widget/page_content.html' %}
 

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

@@ -96,7 +96,7 @@
 </div><!-- /#wrapper -->
 
 {% block fixed-controls %}
-<div id="grw-fab-container"></div>
+<div id="grw-fab-container" data-testid="grw-fab-container"></div>
 {% endblock %}
 
 <div id="grw-hotkeys-manager"></div>

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

@@ -19,11 +19,8 @@ context('Access to page', () => {
   it('/Sandbox with anchor hash is successfully loaded', () => {
     cy.visit('/Sandbox#Headers');
 
-    // wait until opacity is 1.
-    cy.getByTestid('grw-fab-create-page')
-      .should('be.visible')
-      .should('have.class', 'fadeInUp')
-      .should('have.css', 'opacity', '1');
+    // hide fab
+    cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });

+ 0 - 32
packages/app/test/cypress/integration/6-home-settings/access-home.spec.ts

@@ -1,32 +0,0 @@
-/* eslint-disable cypress/no-unnecessary-waiting */
-context('Access Home', () => {
-  const ssPrefix = 'access-home-';
-
-  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('Visit home', () => {
-    cy.visit('/');
-    cy.get('.grw-personal-dropdown').click();
-    cy.get('.grw-personal-dropdown .dropdown-menu .btn-group > .btn-outline-secondary:eq(0)').click();
-
-    cy.wait(1500);
-    cy.screenshot(`${ssPrefix}-visit-home`, { capture: 'viewport' });
-  });
-
-});

+ 0 - 85
packages/app/test/cypress/integration/6-home-settings/access-user-settings.spec.ts

@@ -1,85 +0,0 @@
-/* eslint-disable cypress/no-unnecessary-waiting */
-context('Access User settings', () => {
-  const ssPrefix = 'access-user-settings-';
-
-  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;
-    });
-
-    cy.visit('/');
-    cy.get('.grw-personal-dropdown').click();
-    cy.get('[href="/me"]').click();
-
-    cy.wait(1500);
-  });
-
-  beforeEach(() => {
-    if (connectSid != null) {
-      cy.setCookie('connect.sid', connectSid);
-    }
-  });
-
-  it('Update settings', () => {
-    // Access User information
-    cy.get('#personal-setting .tab-pane.active > div:first button').click(); // Click basic info update button
-
-    cy.wait(500);
-
-    cy.screenshot(`${ssPrefix}-user-information`, { capture: 'viewport' });
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
-
-    // Access External account
-    cy.get('#personal-setting .nav-title.nav li:eq(1) a').click(); // click
-    cy.get('#personal-setting .tab-pane.active h2 button').click(); // click add button
-    cy.get('.modal-footer button').click(); // click add button in modal form
-    cy.get('.close[aria-label="Close"]').click(); // close modal form
-
-    cy.wait(500);
-
-    cy.screenshot(`${ssPrefix}-external-account`, { capture: 'viewport' });
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
-
-    // Access Password setting
-    cy.get('#personal-setting .nav-title.nav li:eq(2) a').click();
-    cy.get('#personal-setting .tab-pane.active button').click(); // click update button
-
-    cy.wait(500);
-
-    cy.screenshot(`${ssPrefix}-password-setting`, { capture: 'viewport' });
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
-
-    // Access API setting
-    cy.get('#personal-setting .nav-title.nav li:eq(2) a').click();
-    cy.get('#personal-setting .tab-pane.active button').click(); // click update API token button
-
-    cy.wait(500);
-
-    cy.screenshot(`${ssPrefix}-api-setting`, { capture: 'viewport' });
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
-
-    // Access Editor setting
-    cy.get('#personal-setting .nav-title.nav li:eq(3) a').click();
-    cy.get('#personal-setting .tab-pane.active button').click(); // click update button
-
-    cy.wait(500);
-
-    cy.screenshot(`${ssPrefix}-editor-setting`, { capture: 'viewport' });
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
-
-    // Access In-app notification setting
-    cy.get('#personal-setting .nav-title.nav li:eq(4) a').click();
-    cy.get('#personal-setting .tab-pane.active button').click(); // click update button
-
-    cy.wait(500);
-
-    cy.screenshot(`${ssPrefix}-in-app-notification-setting`, { capture: 'viewport' });
-  });
-
-});

+ 101 - 0
packages/app/test/cypress/integration/6-home/home.spec.ts

@@ -0,0 +1,101 @@
+context('Access Home', () => {
+  const ssPrefix = 'home-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+  });
+
+  it('Visit home', () => {
+    cy.visit('/dummy');
+    cy.getByTestid('grw-personal-dropdown').click();
+    cy.getByTestid('grw-personal-dropdown').find('.dropdown-menu .btn-group > .btn-outline-secondary:eq(0)').click();
+
+    cy.getByTestid('grw-user-page').should('be.visible');
+
+    cy.screenshot(`${ssPrefix}-visit-home`);
+  });
+
+});
+
+
+context('Access User settings', () => {
+  const ssPrefix = 'access-user-settings-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+  });
+
+  it('Update settings', () => {
+    cy.visit('/me');
+
+    // hide fab
+    cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
+
+    // User information
+    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.screenshot(`${ssPrefix}-user-information-2`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access External account
+    cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(1) a').click();
+    cy.scrollTo('top');
+    cy.screenshot(`${ssPrefix}-external-account-1`);
+    cy.getByTestid('grw-external-account-add-button').click();
+    cy.getByTestid('grw-associate-modal').should('be.visible');
+    cy.screenshot(`${ssPrefix}-external-account-2`);
+    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.screenshot(`${ssPrefix}-external-account-4`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // 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.screenshot(`${ssPrefix}-password-settings-2`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access API setting
+    cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(3) a').click();
+    cy.scrollTo('top');
+    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.screenshot(`${ssPrefix}-api-setting-2`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access Editor setting
+    cy.getByTestid('grw-personal-settings').find('.nav-title.nav li:eq(4) a').click();
+    cy.scrollTo('top');
+    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.screenshot(`${ssPrefix}-editor-setting-2`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // 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.screenshot(`${ssPrefix}-in-app-notification-setting-2`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+  });
+
+});

+ 11 - 0
packages/app/test/cypress/plugins/index.ts

@@ -19,4 +19,15 @@
 module.exports = (on, config) => {
   // `on` is used to hook into various events Cypress emits
   // `config` is the resolved Cypress config
+
+  // change screen size
+  // see: https://docs.cypress.io/api/plugins/browser-launch-api#Set-screen-size-when-running-headless
+  on('before:browser:launch', (browser, launchOptions) => {
+    if (browser.name === 'chrome' && browser.isHeadless) {
+      launchOptions.args.push('--window-size=1400,1024')
+      launchOptions.args.push('--force-device-scale-factor=1')
+    }
+
+    return launchOptions
+  })
 }