Răsfoiți Sursa

Add Cypress React component testing (#6974)

GitOrigin-RevId: 1260312a0644f3bc60e007a840045974336e264d
Alf Eaton 4 ani în urmă
părinte
comite
53324b0cd2

Fișier diff suprimat deoarece este prea mare
+ 607 - 18
package-lock.json


+ 8 - 1
services/web/.eslintrc

@@ -102,9 +102,16 @@
         }]
       }
     },
+    {
+      // Cypress specific rules
+      "files": ["cypress/**/*.js", "**/test/frontend/**/*.spec.js"],
+      "extends": [
+        "plugin:cypress/recommended"
+      ]
+    },
     {
       // Frontend specific rules
-      "files": ["**/frontend/js/**/*.{js,ts,tsx}", "**/frontend/stories/**/*.{js,ts,tsx}", "**/*.stories.{js,ts,tsx}", "**/test/frontend/**/*.{js,ts,tsx}"],
+      "files": ["**/frontend/js/**/*.{js,ts,tsx}", "**/frontend/stories/**/*.{js,ts,tsx}", "**/*.stories.{js,ts,tsx}", "**/test/frontend/**/*.{js,ts,tsx}", "**/test/frontend/components/**/*.spec.{js,ts,tsx}"],
       "env": {
         "browser": true
       },

+ 5 - 0
services/web/.gitignore

@@ -81,6 +81,11 @@ modules/**/Makefile
 .idea
 .run
 
+# Cypress
+cypress/screenshots/
+cypress/videos/
+cypress/downloads/
+
 # Test fixture zip
 !modules/admin-panel/test/unit/src/data/track-changes-project.zip
 

+ 2 - 0
services/web/Dockerfile

@@ -18,6 +18,8 @@ COPY package.json package-lock.json /overleaf/
 COPY services/web/package.json /overleaf/services/web/
 COPY libraries/ /overleaf/libraries/
 
+ENV CYPRESS_INSTALL_BINARY=0
+
 RUN cd /overleaf && npm ci --quiet
 
 

+ 9 - 0
services/web/Dockerfile.frontend_ct

@@ -0,0 +1,9 @@
+ARG PROJECT_NAME
+ARG BRANCH_NAME
+ARG BUILD_NUMBER
+ARG CYPRESS_IMAGE
+
+FROM ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER as dev
+
+FROM $CYPRESS_IMAGE
+COPY --from=dev /overleaf /overleaf

+ 10 - 1
services/web/Makefile

@@ -60,7 +60,7 @@ clean_ci:
 # Tests
 #
 
-test: test_unit test_karma test_acceptance test_frontend
+test: test_unit test_karma test_acceptance test_frontend test_frontend_ct
 
 test_module: test_unit_module test_acceptance_module
 
@@ -137,6 +137,15 @@ test_frontend:
 	COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_frontend
 	COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
 
+#
+# Frontend component tests in Cypress
+#
+
+test_frontend_ct:
+	COMPOSE_PROJECT_NAME=frontend_test_ct_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
+	COMPOSE_PROJECT_NAME=frontend_test_ct_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_frontend_ct
+	COMPOSE_PROJECT_NAME=frontend_test_ct_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
+
 #
 # Acceptance tests
 #

+ 12 - 0
services/web/cypress.json

@@ -0,0 +1,12 @@
+{
+  "component": {
+    "componentFolder": ".",
+    "testFiles": "./{test,modules/**/test}/frontend/components/**/*.spec.js",
+    "supportFile": "cypress/support/ct/index.js"
+  },
+  "experimentalFetchPolyfill": true,
+  "fixturesFolder": false,
+  "video": false,
+  "viewportHeight": 800,
+  "viewportWidth": 800
+}

+ 19 - 0
services/web/cypress/plugins/index.js

@@ -0,0 +1,19 @@
+module.exports = (on, config) => {
+  if (config.testingType === 'component') {
+    const { startDevServer } = require('@cypress/webpack-dev-server')
+    const merge = require('webpack-merge')
+    const path = require('path')
+    const devConfig = require('../../webpack.config.dev')
+
+    const webpackConfig = merge(devConfig, {
+      devServer: {
+        contentBase: path.join(__dirname, '../../../../public'),
+        stats: 'none',
+      },
+    })
+
+    on('dev-server:start', options => {
+      return startDevServer({ options, webpackConfig })
+    })
+  }
+}

+ 4 - 0
services/web/cypress/support/ct/i18n.js

@@ -0,0 +1,4 @@
+window.i18n = { currentLangCode: 'en' }
+window.ExposedSettings = { appName: 'Overleaf' }
+
+require('../../../frontend/js/i18n')

+ 3 - 0
services/web/cypress/support/ct/index.js

@@ -0,0 +1,3 @@
+require('../../../frontend/stylesheets/style.less')
+require('../shared/exceptions')
+require('./i18n')

+ 6 - 0
services/web/cypress/support/shared/exceptions.js

@@ -0,0 +1,6 @@
+Cypress.on('uncaught:exception', err => {
+  // don't fail the test for ResizeObserver error messages
+  if (err.message.includes('ResizeObserver')) {
+    return false
+  }
+})

+ 15 - 0
services/web/docker-compose.ci.yml

@@ -63,6 +63,21 @@ services:
     environment:
       NODE_OPTIONS: "--unhandled-rejections=strict"
 
+  test_frontend_ct:
+    build:
+      context: .
+      dockerfile: ./Dockerfile.frontend_ct
+      args:
+        PROJECT_NAME: $PROJECT_NAME
+        BRANCH_NAME: $BRANCH_NAME
+        BUILD_NUMBER: $BUILD_NUMBER
+        CYPRESS_IMAGE: $CYPRESS_IMAGE
+    working_dir: /overleaf/services/web
+    entrypoint: npm
+    command:
+      - "run"
+      - "cypress:run-ct"
+
   tar:
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER-webpack
     volumes:

+ 10 - 0
services/web/docker-compose.yml

@@ -74,6 +74,16 @@ services:
     command: npm run --silent test:frontend
     user: node
 
+  test_frontend_ct:
+    image: cypress/included:9.5.2
+    volumes:
+      - ../../:/overleaf
+    working_dir: /overleaf/services/web
+    entrypoint: npm
+    command:
+      - "run"
+      - "cypress:run-ct"
+
   redis:
     image: redis
 

+ 9 - 1
services/web/package.json

@@ -13,7 +13,7 @@
     "test:unit:all": "npm run test:unit:run_dir -- test/unit/src modules/*/test/unit/src",
     "test:unit:all:silent": "npm run test:unit:all -- --reporter dot",
     "test:unit:app": "npm run test:unit:run_dir -- test/unit/src",
-    "test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js test/frontend modules/*/test/frontend",
+    "test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,ts,tsx}' test/frontend modules/*/test/frontend",
     "test:frontend:coverage": "c8 --all --include 'frontend/js' --include 'modules/*/frontend/js' --exclude 'frontend/js/vendor' --reporter=lcov --reporter=text-summary npm run test:frontend",
     "test:karma": "karma start",
     "test:karma:single": "karma start --no-auto-watch --single-run",
@@ -33,6 +33,8 @@
     "migrations": "east",
     "storybook": "start-storybook -p 6006",
     "convert-themes": "node modules/source-editor/frontend/js/themes/convert.js",
+    "cypress:open-ct": "SHARELATEX_CONFIG=$PWD/config/settings.webpack.js cypress open-ct",
+    "cypress:run-ct": "SHARELATEX_CONFIG=$PWD/config/settings.webpack.js cypress run-ct",
     "routes": "bin/routes"
   },
   "browserslist": [
@@ -208,10 +210,14 @@
   },
   "devDependencies": {
     "@babel/register": "^7.14.5",
+    "@cypress/react": "^5.12.1",
+    "@cypress/webpack-preprocessor": "^5.11.1",
+    "@cypress/webpack-dev-server": "^1.8.0",
     "@juggle/resize-observer": "^3.3.1",
     "@storybook/addon-a11y": "^6.4.19",
     "@storybook/addon-essentials": "^6.4.19",
     "@storybook/react": "^6.4.19",
+    "@testing-library/cypress": "^8.0.2",
     "@testing-library/dom": "^7.31.2",
     "@testing-library/react": "^11.2.7",
     "@testing-library/react-hooks": "^7.0.0",
@@ -239,6 +245,7 @@
     "content-disposition": "^0.5.0",
     "copy-webpack-plugin": "^5.1.1",
     "css-loader": "^3.5.2",
+    "cypress": "^9.5.1",
     "es6-promise": "^4.2.8",
     "escodegen": "^2.0.0",
     "eslint": "^8.9.0",
@@ -247,6 +254,7 @@
     "eslint-config-standard-jsx": "^11.0.0-0",
     "eslint-plugin-chai-expect": "^2.2.0",
     "eslint-plugin-chai-friendly": "^0.7.2",
+    "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-import": "^2.25.3",
     "eslint-plugin-jsx-a11y": "^6.5.1",
     "eslint-plugin-mocha": "^9.0.0",

+ 18 - 0
services/web/test/frontend/components/shared/beta-badge.spec.js

@@ -0,0 +1,18 @@
+import { mount } from '@cypress/react'
+import BetaBadge from '../../../../frontend/js/shared/components/beta-badge'
+
+describe('beta badge', function () {
+  it('renders the url and tooltip text', function () {
+    mount(
+      <BetaBadge
+        url="/foo"
+        tooltip={{
+          id: 'test-tooltip',
+          text: 'This is a test',
+        }}
+      />
+    )
+
+    cy.get('a[href="/foo"]').contains('This is a test')
+  })
+})

+ 1 - 1
services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.js

@@ -13,9 +13,9 @@ import ShareProjectModal from '../../../../../frontend/js/features/share-project
 import {
   renderWithEditorContext,
   cleanUpContext,
-  EditorProviders,
 } from '../../../helpers/render-with-context'
 import * as locationModule from '../../../../../frontend/js/features/share-project-modal/utils/location'
+import { EditorProviders } from '../../../helpers/editor-providers'
 
 describe('<ShareProjectModal/>', function () {
   const project = {

+ 123 - 0
services/web/test/frontend/helpers/editor-providers.js

@@ -0,0 +1,123 @@
+// Disable prop type checks for test harnesses
+/* eslint-disable react/prop-types */
+import sinon from 'sinon'
+import { get } from 'lodash'
+import { SplitTestProvider } from '../../../frontend/js/shared/context/split-test-context'
+import { IdeProvider } from '../../../frontend/js/shared/context/ide-context'
+import { UserProvider } from '../../../frontend/js/shared/context/user-context'
+import { ProjectProvider } from '../../../frontend/js/shared/context/project-context'
+import { FileTreeDataProvider } from '../../../frontend/js/shared/context/file-tree-data-context'
+import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
+import { DetachProvider } from '../../../frontend/js/shared/context/detach-context'
+import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
+import { CompileProvider } from '../../../frontend/js/shared/context/compile-context'
+
+// these constants can be imported in tests instead of
+// using magic strings
+export const PROJECT_ID = 'project123'
+export const PROJECT_NAME = 'project-name'
+
+export function EditorProviders({
+  user = { id: '123abd', email: 'testuser@example.com' },
+  projectId = PROJECT_ID,
+  rootDocId = '_root_doc_id',
+  socket = {
+    on: sinon.stub(),
+    removeListener: sinon.stub(),
+  },
+  isRestrictedTokenMember = false,
+  clsiServerId = '1234',
+  scope,
+  features = {
+    referencesSearch: true,
+  },
+  permissionsLevel = 'owner',
+  children,
+  rootFolder = [
+    {
+      _id: 'root-folder-id',
+      name: 'rootFolder',
+      docs: [],
+      folders: [],
+      fileRefs: [],
+    },
+  ],
+  ui = { view: null, pdfLayout: 'flat', chatOpen: true },
+  fileTreeManager = {
+    findEntityById: () => null,
+    findEntityByPath: () => null,
+    getEntityPath: () => '',
+    getRootDocDirname: () => '',
+  },
+  editorManager = {
+    getCurrentDocId: () => 'foo',
+    getCurrentDocValue: () => {},
+    openDoc: sinon.stub(),
+  },
+}) {
+  window.user = user || window.user
+  window.gitBridgePublicBaseUrl = 'git.overleaf.test'
+  window.project_id = projectId != null ? projectId : window.project_id
+  window.isRestrictedTokenMember = isRestrictedTokenMember
+
+  const $scope = {
+    user: window.user,
+    project: {
+      _id: window.project_id,
+      name: PROJECT_NAME,
+      owner: {
+        _id: '124abd',
+        email: 'owner@example.com',
+      },
+      features,
+      rootDoc_id: rootDocId,
+      rootFolder,
+    },
+    ui,
+    $watch: (path, callback) => {
+      callback(get($scope, path))
+      return () => null
+    },
+    $applyAsync: sinon.stub(),
+    toggleHistory: sinon.stub(),
+    permissionsLevel,
+    ...scope,
+  }
+
+  const metadataManager = {
+    metadata: {
+      state: {
+        documents: {},
+      },
+    },
+  }
+
+  window._ide = {
+    $scope,
+    socket,
+    clsiServerId,
+    editorManager,
+    fileTreeManager,
+    metadataManager,
+  }
+
+  return (
+    <SplitTestProvider>
+      <IdeProvider ide={window._ide}>
+        <UserProvider>
+          <ProjectProvider>
+            <FileTreeDataProvider>
+              <EditorProvider settings={{}}>
+                <DetachProvider>
+                  <LayoutProvider>
+                    <CompileProvider>{children}</CompileProvider>
+                  </LayoutProvider>
+                </DetachProvider>
+              </EditorProvider>
+            </FileTreeDataProvider>
+          </ProjectProvider>
+        </UserProvider>
+      </IdeProvider>
+    </SplitTestProvider>
+  )
+}

+ 1 - 121
services/web/test/frontend/helpers/render-with-context.js

@@ -3,128 +3,8 @@
 
 import { render } from '@testing-library/react'
 import { renderHook } from '@testing-library/react-hooks'
-import sinon from 'sinon'
-import { UserProvider } from '../../../frontend/js/shared/context/user-context'
-import { EditorProvider } from '../../../frontend/js/shared/context/editor-context'
-import { LayoutProvider } from '../../../frontend/js/shared/context/layout-context'
-import { DetachProvider } from '../../../frontend/js/shared/context/detach-context'
 import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
-import { IdeProvider } from '../../../frontend/js/shared/context/ide-context'
-import { get } from 'lodash'
-import { ProjectProvider } from '../../../frontend/js/shared/context/project-context'
-import { SplitTestProvider } from '../../../frontend/js/shared/context/split-test-context'
-import { CompileProvider } from '../../../frontend/js/shared/context/compile-context'
-import { FileTreeDataProvider } from '../../../frontend/js/shared/context/file-tree-data-context'
-
-// these constants can be imported in tests instead of
-// using magic strings
-export const PROJECT_ID = 'project123'
-export const PROJECT_NAME = 'project-name'
-
-export function EditorProviders({
-  user = { id: '123abd', email: 'testuser@example.com' },
-  projectId = PROJECT_ID,
-  rootDocId = '_root_doc_id',
-  socket = {
-    on: sinon.stub(),
-    removeListener: sinon.stub(),
-  },
-  isRestrictedTokenMember = false,
-  clsiServerId = '1234',
-  scope,
-  features = {
-    referencesSearch: true,
-  },
-  permissionsLevel = 'owner',
-  children,
-  rootFolder = [
-    {
-      _id: 'root-folder-id',
-      name: 'rootFolder',
-      docs: [],
-      folders: [],
-      fileRefs: [],
-    },
-  ],
-  ui = { view: null, pdfLayout: 'flat', chatOpen: true },
-  fileTreeManager = {
-    findEntityById: () => null,
-    findEntityByPath: () => null,
-    getEntityPath: () => '',
-    getRootDocDirname: () => '',
-  },
-  editorManager = {
-    getCurrentDocId: () => 'foo',
-    getCurrentDocValue: () => {},
-    openDoc: sinon.stub(),
-  },
-}) {
-  window.user = user || window.user
-  window.gitBridgePublicBaseUrl = 'git.overleaf.test'
-  window.project_id = projectId != null ? projectId : window.project_id
-  window.isRestrictedTokenMember = isRestrictedTokenMember
-
-  const $scope = {
-    user: window.user,
-    project: {
-      _id: window.project_id,
-      name: PROJECT_NAME,
-      owner: {
-        _id: '124abd',
-        email: 'owner@example.com',
-      },
-      features,
-      rootDoc_id: rootDocId,
-      rootFolder,
-    },
-    ui,
-    $watch: (path, callback) => {
-      callback(get($scope, path))
-      return () => null
-    },
-    $applyAsync: sinon.stub(),
-    toggleHistory: sinon.stub(),
-    permissionsLevel,
-    ...scope,
-  }
-
-  const metadataManager = {
-    metadata: {
-      state: {
-        documents: {},
-      },
-    },
-  }
-
-  window._ide = {
-    $scope,
-    socket,
-    clsiServerId,
-    editorManager,
-    fileTreeManager,
-    metadataManager,
-  }
-
-  return (
-    <SplitTestProvider>
-      <IdeProvider ide={window._ide}>
-        <UserProvider>
-          <ProjectProvider>
-            <FileTreeDataProvider>
-              <EditorProvider settings={{}}>
-                <DetachProvider>
-                  <LayoutProvider>
-                    <CompileProvider>{children}</CompileProvider>
-                  </LayoutProvider>
-                </DetachProvider>
-              </EditorProvider>
-            </FileTreeDataProvider>
-          </ProjectProvider>
-        </UserProvider>
-      </IdeProvider>
-    </SplitTestProvider>
-  )
-}
+import { EditorProviders } from './editor-providers'
 
 export function renderWithEditorContext(
   component,

+ 29 - 13
services/web/webpack.config.js

@@ -55,6 +55,8 @@ const aceDir = getModuleDirectory('ace-builds')
 
 const pdfjsVersions = ['pdfjs-dist210', 'pdfjs-dist213']
 
+const vendorDir = path.join(__dirname, 'frontend/js/vendor')
+
 module.exports = {
   // Defines the "entry point(s)" for the application - i.e. the file which
   // bootstraps the application
@@ -64,7 +66,7 @@ module.exports = {
   // Note: webpack-dev-server does not write the bundle to disk, instead it is
   // kept in memory for speed
   output: {
-    path: path.join(__dirname, '/public'),
+    path: path.join(__dirname, 'public'),
 
     publicPath: '/',
 
@@ -86,10 +88,7 @@ module.exports = {
         test: /\.[j|t]sx?$/,
         // Only compile application files (npm and vendored dependencies are in
         // ES5 already)
-        exclude: [
-          /node_modules\/(?!react-dnd\/)/,
-          path.resolve(__dirname, 'frontend/js/vendor'),
-        ],
+        exclude: [/node_modules\/(?!react-dnd\/)/, vendorDir],
         use: [
           {
             loader: 'babel-loader',
@@ -97,6 +96,7 @@ module.exports = {
               // Configure babel-loader to cache compiled output so that
               // subsequent compile runs are much faster
               cacheDirectory: true,
+              configFile: path.join(__dirname, './babel.config.json'),
             },
           },
         ],
@@ -205,7 +205,7 @@ module.exports = {
         test: /locales\/(\w{2}(-\w{2})?)\.json$/,
         use: [
           {
-            loader: path.resolve('frontend/translations-loader.js'),
+            loader: path.join(__dirname, 'frontend/translations-loader.js'),
           },
         ],
       },
@@ -310,15 +310,31 @@ module.exports = {
       }
     ),
 
-    new CopyPlugin([
+    new CopyPlugin(
+      [
+        {
+          from: 'libs/sigma-master',
+          to: 'js/libs/sigma-master',
+        },
+      ],
       {
-        from: 'frontend/js/vendor/libs/sigma-master',
-        to: 'js/libs/sigma-master',
-      },
+        context: vendorDir,
+      }
+    ),
+
+    new CopyPlugin(
+      [
+        {
+          from: 'src-min-noconflict',
+          to: `js/ace-${PackageVersions.version.ace}/`,
+        },
+      ],
       {
-        from: `${aceDir}/src-min-noconflict`,
-        to: `js/ace-${PackageVersions.version.ace}/`,
-      },
+        context: aceDir,
+      }
+    ),
+
+    new CopyPlugin([
       ...pdfjsVersions.flatMap(version => {
         const dir = getModuleDirectory(version)
 

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff