Add Cypress React component testing (#6974)

GitOrigin-RevId: 1260312a0644f3bc60e007a840045974336e264d
This commit is contained in:
Alf Eaton 2022-03-30 10:49:41 +01:00 committed by Copybot
parent 76beba4393
commit 53324b0cd2
19 changed files with 3209 additions and 388 deletions

3173
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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
},

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
services/web/cypress.json Normal file
View file

@ -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
}

View file

@ -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 })
})
}
}

View file

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

View file

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

View file

@ -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
}
})

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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')
})
})

View file

@ -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 = {

View file

@ -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>
)
}

View file

@ -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,

View file

@ -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(
[
{
from: 'libs/sigma-master',
to: 'js/libs/sigma-master',
},
],
{
context: vendorDir,
}
),
new CopyPlugin(
[
{
from: 'src-min-noconflict',
to: `js/ace-${PackageVersions.version.ace}/`,
},
],
{
context: aceDir,
}
),
new CopyPlugin([
{
from: 'frontend/js/vendor/libs/sigma-master',
to: 'js/libs/sigma-master',
},
{
from: `${aceDir}/src-min-noconflict`,
to: `js/ace-${PackageVersions.version.ace}/`,
},
...pdfjsVersions.flatMap(version => {
const dir = getModuleDirectory(version)