diff --git a/.github/workflows/ci-e2e-ui.yml b/.github/workflows/ci-e2e-ui.yml index f2fcc8294..f61dd0fc3 100644 --- a/.github/workflows/ci-e2e-ui.yml +++ b/.github/workflows/ci-e2e-ui.yml @@ -1,6 +1,18 @@ name: E2E UI -on: [pull_request] +on: + push: + branches: + - 5.x + - 'changeset-release/5.x' + pull_request: + paths: + - .github/workflows/ci.yml + - 'packages/**' + - 'jest/**' + - 'package.json' + - 'pnpm-workspace.yaml' + - 'test/**' jobs: ci: @@ -16,12 +28,18 @@ jobs: steps: - uses: actions/checkout@v2.3.1 - name: Use Node ${{ matrix.node_version }} - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v1 with: - node-version: ${{ matrix.node_version }} + node_version: ${{ matrix.node_version }} + - name: Install pnpm + run: npm i -g pnpm@latest - name: Install - run: yarn install --immutable + run: pnpm recursive install + - name: Clean + run: pnpm clean - name: Build - run: yarn code:build + run: pnpm build - name: Test UI - run: yarn run test:e2e + run: pnpm test:e2e:ui + env: + DEBUG: verdaccio:e2e* diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index d3600321b..fddfe140f 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -1,4 +1,4 @@ -name: CI E2E +name: E2E CLI on: push: @@ -7,12 +7,12 @@ on: - 'changeset-release/5.x' pull_request: paths: - - .changeset/** - .github/workflows/ci.yml - 'packages/**' - 'jest/**' - 'package.json' - 'pnpm-workspace.yaml' + - 'test/**' jobs: ci: @@ -39,7 +39,7 @@ jobs: run: pnpm clean - name: Build run: pnpm build - - name: Test + - name: Test CLI run: pnpm test:e2e:cli env: DEBUG: verdaccio:e2e* diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 77629d9f2..79f4ee27f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,7 +5,11 @@ on: branches-ignore: - 5.x - 'changeset-release/5.x' + - 'dev/**' pull_request: + paths: + - .github/workflows/ci.yml + - 'packages/**' schedule: - cron: '0 2 * * 4' diff --git a/.prettierignore b/.prettierignore index d5c32c6b0..6babbd67a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,6 +13,7 @@ CHANGELOG.md CONTRIBUTORS.md node_modules/ +**/coverage/** **/static/*.js **/build/*.js packages/core/local-storage/_storage/** diff --git a/package.json b/package.json index b36d55059..2cc469eaa 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"", "test": "pnpm recursive test --filter ./packages", "test:e2e:cli": "pnpm test --filter ...@verdaccio/e2e-cli", + "test:e2e:ui": "pnpm test --filter ...@verdaccio/e2e-ui", "start": "concurrently --kill-others \"pnpm start:server\" \"pnpm start:web\"", "start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000", "start:web": "pnpm start --filter ...@verdaccio/ui-theme", diff --git a/packages/plugins/ui-theme/package.json b/packages/plugins/ui-theme/package.json index 54761e1d4..75829244e 100644 --- a/packages/plugins/ui-theme/package.json +++ b/packages/plugins/ui-theme/package.json @@ -25,7 +25,6 @@ "@testing-library/dom": "^7.29.0", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "10.4.9", - "@verdaccio/commons-api": "workspace:10.0.0-alpha.1", "@verdaccio/node-api": "workspace:5.0.0-alpha.1", "autosuggest-highlight": "3.1.1", "babel-plugin-dynamic-import-node": "^2.3.3", @@ -56,11 +55,7 @@ "normalize.css": "8.0.1", "optimize-css-assets-webpack-plugin": "^5.0.4", "ora": "4.0.4", - "prop-types": "15.7.2", - "puppeteer": "5.3.1", - "react": "16.13.1", "react-autosuggest": "10.0.2", - "react-dom": "16.13.1", "react-hook-form": "3.29.4", "react-hot-loader": "4.12.21", "react-i18next": "^11.8.3", @@ -70,7 +65,10 @@ "request": "2.88.2", "resolve-url-loader": "3.1.1", "rimraf": "3.0.2", + "react": "16.13.1", + "react-dom": "16.13.1", "source-map-loader": "1.1.0", + "prop-types": "15.7.2", "standard-version": "9.0.0", "style-loader": "1.2.1", "stylelint": "13.7.2", @@ -124,7 +122,6 @@ "type-check": "tsc --noEmit -p tsconfig.build.json", "start": "babel-node tools/dev.server.js", "test:clean": "jest --clearCache", - "test:e2e": " jest --config ./test/jest.config.e2e.js", "test": "cross-env BABEL_ENV=test cross-env NODE_ENV=test cross-env TZ=UTC jest --config ./jest/jest.config.js --maxWorkers 2 --passWithNoTests", "test:update-snapshot": "yarn run test -- -u", "test:size": "bundlesize", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70f0261da..adf4aafab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -640,7 +640,6 @@ importers: '@testing-library/dom': 7.29.0 '@testing-library/jest-dom': 5.11.6 '@testing-library/react': 10.4.9_react-dom@16.13.1+react@16.13.1 - '@verdaccio/commons-api': 'link:../../core/commons-api' '@verdaccio/node-api': 'link:../../node-api' autosuggest-highlight: 3.1.1 babel-loader: 8.2.2_webpack@5.10.1 @@ -672,7 +671,6 @@ importers: optimize-css-assets-webpack-plugin: 5.0.4_webpack@5.10.1 ora: 4.0.4 prop-types: 15.7.2 - puppeteer: 5.3.1 react: 16.13.1 react-autosuggest: 10.0.2_react@16.13.1 react-dom: 16.13.1_react@16.13.1 @@ -720,7 +718,6 @@ importers: '@testing-library/dom': ^7.29.0 '@testing-library/jest-dom': ^5.11.6 '@testing-library/react': 10.4.9 - '@verdaccio/commons-api': 'workspace:10.0.0-alpha.1' '@verdaccio/node-api': 'workspace:5.0.0-alpha.1' autosuggest-highlight: 3.1.1 babel-loader: ^8.2.2 @@ -752,7 +749,6 @@ importers: optimize-css-assets-webpack-plugin: ^5.0.4 ora: 4.0.4 prop-types: 15.7.2 - puppeteer: 5.3.1 react: 16.13.1 react-autosuggest: 10.0.2 react-dom: 16.13.1 @@ -982,6 +978,27 @@ importers: request: ^2.88.2 semver: ^7.3.4 yarn: 1.22.10 + test/e2e-ui: + devDependencies: + '@verdaccio/commons-api': 'link:../../packages/core/commons-api' + '@verdaccio/ui-theme': 'link:../../packages/plugins/ui-theme' + debug: 4.3.1 + kleur: 4.1.3 + lodash: 4.17.20 + mkdirp: 1.0.4 + puppeteer: 5.5.0 + request: 2.88.2 + rimraf: 3.0.2 + specifiers: + '@verdaccio/commons-api': 'workspace:10.0.0-alpha.1' + '@verdaccio/ui-theme': 'workspace:5.0.0-alpha.1' + debug: 4.3.1 + kleur: ^4.1.3 + lodash: ^4.17.20 + mkdirp: ^1.0.4 + puppeteer: ^5.5.0 + request: ^2.88.2 + rimraf: ^3.0.2 website: dependencies: '@emotion/core': 10.0.28_react@16.13.1 @@ -11291,10 +11308,10 @@ packages: dev: false resolution: integrity: sha512-7/nIzKdQ8y2K0imjIP7dyg2GJ2h38Ps6VOMXWZHIarNDV3p6mTXyEugKFnkmsZ2DD58JEG34ILyVb3qdOMmP9w== - /devtools-protocol/0.0.799653: + /devtools-protocol/0.0.818844: dev: true resolution: - integrity: sha512-t1CcaZbvm8pOlikqrsIM9GOa7Ipp07+4h/q9u0JXBWjPCjHdBl9KkddX87Vv9vBHoBGtwV79sYQNGnQM6iS5gg== + integrity: sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg== /diacritic/0.0.2: dev: true resolution: @@ -17586,6 +17603,12 @@ packages: node: '>=6' resolution: integrity: sha512-FGCCxczbrZuF5CtMeO0xfnjhzkVZSXfcWK90IPLucDWZwskrpYN7pmRIgvd8muU0mrPrzy4A2RBGuwCjLHI+nw== + /kleur/4.1.3: + dev: true + engines: + node: '>=6' + resolution: + integrity: sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw== /known-css-properties/0.19.0: dev: true resolution: @@ -21506,12 +21529,13 @@ packages: node: '>=8' resolution: integrity: sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== - /puppeteer/5.3.1: + /puppeteer/5.5.0: dependencies: debug: 4.3.1 - devtools-protocol: 0.0.799653 + devtools-protocol: 0.0.818844 extract-zip: 2.0.1 https-proxy-agent: 4.0.0 + node-fetch: 2.6.1 pkg-dir: 4.2.0 progress: 2.0.3 proxy-from-env: 1.1.0 @@ -21524,7 +21548,7 @@ packages: node: '>=10.18.1' requiresBuild: true resolution: - integrity: sha512-YTM1RaBeYrj6n7IlRXRYLqJHF+GM7tasbvrNFx6w1S16G76NrPq7oYFKLDO+BQsXNtS8kW2GxWCXjIMPvfDyaQ== + integrity: sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg== /q/1.5.1: engines: node: '>=0.6.0' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9b874fae2..bfa78b9b3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,4 @@ packages: - packages/core/* - packages/plugins/* - website - - test/e2e-cli + - test/e2e-* diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..acc25ad68 --- /dev/null +++ b/test/README.md @@ -0,0 +1,6 @@ +# E2E Testing + +This folder is composed of two strategies: + +- E2E for CLI +- E2E for the UI (puppeteer) diff --git a/test/e2e-cli/README.md b/test/e2e-cli/README.md index 102b81897..e942b766f 100644 --- a/test/e2e-cli/README.md +++ b/test/e2e-cli/README.md @@ -1,4 +1,4 @@ -# E2E Testing +# E2E CLI Testing ## What is included on these test? diff --git a/test/e2e-ui/.babelrc b/test/e2e-ui/.babelrc new file mode 100644 index 000000000..633f93f42 --- /dev/null +++ b/test/e2e-ui/.babelrc @@ -0,0 +1,3 @@ +{ + "extends": "../../.babelrc" +} diff --git a/test/e2e-ui/.eslintrc b/test/e2e-ui/.eslintrc new file mode 100644 index 000000000..3811bf06c --- /dev/null +++ b/test/e2e-ui/.eslintrc @@ -0,0 +1,15 @@ +{ + "rules": { + "no-console": 0, + "new-cap": 0, + "max-len": 0 + }, + "globals": { + "VERDACCIO_API_URL": true, + "__DEBUG__": true + }, + "env": { + "browser": true, + "jest/globals": true + } +} diff --git a/test/e2e-ui/README.md b/test/e2e-ui/README.md new file mode 100644 index 000000000..d409f2b45 --- /dev/null +++ b/test/e2e-ui/README.md @@ -0,0 +1,9 @@ +# E2E UI Testing + +## What is included on these test? + +- Run acceptance test on UI + - Check home page works correctly + - Check navigation + - Check sidebar + - Check protected packages works diff --git a/test/e2e-ui/config/config-protected-e2e.yaml b/test/e2e-ui/config/config-protected-e2e.yaml new file mode 100644 index 000000000..e39da3782 --- /dev/null +++ b/test/e2e-ui/config/config-protected-e2e.yaml @@ -0,0 +1,27 @@ +web: + enable: true + title: verdaccio-server-protected-e2e + +store: + memory: + limit: 10 + +auth: + auth-memory: + users: + test: + name: test + password: test + +logs: + - { type: stdout, format: pretty, level: info } + +packages: + 'protected-*': + access: $authenticated + publish: $authenticated + +listen: 0.0.0.0:55552 + +# expose internal methods +_debug: true diff --git a/test/e2e-ui/config/config-scoped-e2e.yaml b/test/e2e-ui/config/config-scoped-e2e.yaml new file mode 100644 index 000000000..a5a80c5c6 --- /dev/null +++ b/test/e2e-ui/config/config-scoped-e2e.yaml @@ -0,0 +1,30 @@ +web: + enable: true + title: verdaccio-server-e2e + +store: + memory: + limit: 10 + +auth: + auth-memory: + users: + test: + name: test + password: test + +logs: + - { type: stdout, format: pretty, level: info } + +packages: + '@*/*': + access: $all + publish: $all + '**': + access: $all + publish: $authenticated + +listen: 0.0.0.0:55558 + +# expose internal methods +_debug: true diff --git a/test/e2e-ui/e2e.spec.js b/test/e2e-ui/e2e.spec.js new file mode 100644 index 000000000..d92e6b329 --- /dev/null +++ b/test/e2e-ui/e2e.spec.js @@ -0,0 +1,213 @@ +const protectedPackageMetadata = require('./partials/pkg-protected'); +const scopedPackageMetadata = require('./partials/pkg-scoped'); + +describe('/ (Verdaccio Page)', () => { + let page; + // this might be increased based on the delays included in all test + jest.setTimeout(20000); + + const clickElement = async function (selector, options = { delay: 100 }) { + const button = await page.$(selector); + await button.focus(); + await button.click(options); + }; + + const evaluateSignIn = async function (matchText = 'Login') { + const text = await page.evaluate(() => { + return document.querySelector('button[data-testid="header--button-login"]').textContent; + }); + + expect(text).toMatch(matchText); + }; + + const getPackages = async function () { + return await page.$$('.package-title'); + }; + + const logIn = async function () { + await clickElement('button[data-testid="header--button-login"]'); + // we fill the sign in form + const signInDialog = await page.$('#login--dialog'); + const userInput = await signInDialog.$('#login--dialog-username'); + expect(userInput).not.toBeNull(); + const passInput = await signInDialog.$('#login--dialog-password'); + expect(passInput).not.toBeNull(); + await userInput.type('test', { delay: 100 }); + await passInput.type('test', { delay: 100 }); + await passInput.dispose(); + // click on log in + const loginButton = await page.$('#login--dialog-button-submit'); + expect(loginButton).toBeDefined(); + await loginButton.focus(); + await loginButton.click({ delay: 100 }); + await page.waitFor(500); + }; + + beforeAll(async () => { + page = await global.__BROWSER__.newPage(); + await page.goto('http://0.0.0.0:55558'); + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + }); + + afterAll(async () => { + await page.close(); + }); + + test('should display title', async () => { + const text = await page.title(); + await page.waitFor(1000); + + expect(text).toContain('verdaccio-server-e2e'); + }); + // + + test('should match title with no packages published', async () => { + const text = await page.evaluate(() => document.querySelector('#help-card__title').textContent); + expect(text).toMatch('No Package Published Yet.'); + }); + // + + test('should match title with first step', async () => { + const text = await page.evaluate(() => document.querySelector('#help-card').textContent); + expect(text).toContain('npm adduser --registry http://0.0.0.0:55558'); + }); + // + + test('should match title with second step', async () => { + const text = await page.evaluate(() => document.querySelector('#help-card').textContent); + expect(text).toContain('npm publish --registry http://0.0.0.0:55558'); + }); + // + + test('should match button Login to sign in', async () => { + await evaluateSignIn(); + }); + // + + test('should click on sign in button', async () => { + const signInButton = await page.$('button[data-testid="header--button-login"]'); + await signInButton.click(); + await page.waitFor(1000); + const signInDialog = await page.$('#login--dialog'); + expect(signInDialog).not.toBeNull(); + const closeButton = await page.$('button[data-testid="close-login-dialog-button"]'); + await closeButton.click(); + await page.waitFor(500); + }); + // + + test('should log in an user', async () => { + // we open the dialog + await logIn(); + // check whether user is logged + const buttonLogout = await page.$('#header--button-logout'); + expect(buttonLogout).toBeDefined(); + }); + + test('should logout an user', async () => { + // we assume the user is logged already + await clickElement('#header--button-account', { delay: 500 }); + await page.waitFor(1000); + await clickElement('#header--button-logout > span', { delay: 500 }); + await page.waitFor(1000); + await evaluateSignIn(); + }); + // + + test('should check registry info dialog', async () => { + const registryInfoButton = await page.$('#header--button-registryInfo'); + registryInfoButton.click(); + await page.waitFor(500); + + const registryInfoDialog = await page.$('#registryInfo--dialog-container'); + expect(registryInfoDialog).not.toBeNull(); + + const closeButton = await page.$('#registryInfo--dialog-close'); + await closeButton.click(); + }); + // + + test('should publish a package', async () => { + await global.__SERVER__.putPackage(scopedPackageMetadata.name, scopedPackageMetadata); + await page.waitFor(1000); + await page.reload(); + await page.waitFor(1000); + const packagesList = await getPackages(); + expect(packagesList).toHaveLength(1); + }); + // + + test('should navigate to the package detail', async () => { + const packagesList = await getPackages(); + // console.log("-->packagesList:", packagesList); + const firstPackage = packagesList[0]; + await firstPackage.click({ delay: 200 }); + await page.waitFor(1000); + const readmeText = await page.evaluate( + () => document.querySelector('.markdown-body').textContent + ); + + expect(readmeText).toMatch('test'); + }); + + test('should contains last sync information', async () => { + const versionList = await page.$$('.sidebar-info .detail-info'); + expect(versionList).toHaveLength(1); + }); + // + + test('should display dependencies tab', async () => { + const dependenciesTab = await page.$$('#dependencies-tab'); + expect(dependenciesTab).toHaveLength(1); + await dependenciesTab[0].click({ delay: 200 }); + await page.waitFor(1000); + const tags = await page.$$('.dep-tag'); + const tag = tags[0]; + const label = await page.evaluate((el) => el.innerText, tag); + expect(label).toMatch('verdaccio@'); + }); + + test('should display version tab', async () => { + const versionsTab = await page.$$('#versions-tab'); + expect(versionsTab).toHaveLength(1); + await versionsTab[0].click({ delay: 200 }); + await page.waitFor(1000); + const versionItems = await page.$$('.version-item'); + expect(versionItems).toHaveLength(2); + }); + + test('should display uplinks tab', async () => { + const upLinksTab = await page.$$('#uplinks-tab'); + expect(upLinksTab).toHaveLength(1); + await upLinksTab[0].click({ delay: 200 }); + await page.waitFor(1000); + }); + + test('should display readme tab', async () => { + const readmeTab = await page.$$('#readme-tab'); + expect(readmeTab).toHaveLength(1); + await readmeTab[0].click({ delay: 200 }); + await page.waitFor(1000); + }); + + test('should publish a protected package', async () => { + await page.goto('http://0.0.0.0:55552'); + await page.waitFor(500); + await global.__SERVER_PROTECTED__.putPackage( + protectedPackageMetadata.name, + protectedPackageMetadata + ); + await page.waitFor(500); + await page.reload(); + await page.waitFor(500); + const text = await page.evaluate(() => document.querySelector('#help-card__title').textContent); + expect(text).toMatch('No Package Published Yet'); + }); + + test('should go to 404 page', async () => { + await page.goto('http://0.0.0.0:55552/-/web/detail/@verdaccio/not-found'); + await page.waitFor(500); + const text = await page.evaluate(() => document.querySelector('.not-found-text').textContent); + expect(text).toMatch("Sorry, we couldn't find it..."); + }); +}); diff --git a/test/e2e-ui/jest.config.e2e.js b/test/e2e-ui/jest.config.e2e.js new file mode 100644 index 000000000..f6197dd42 --- /dev/null +++ b/test/e2e-ui/jest.config.e2e.js @@ -0,0 +1,11 @@ +const config = require('../../jest/config'); + +module.exports = Object.assign({}, config, { + name: 'verdaccio-e2e-jest', + verbose: true, + collectCoverage: false, + globalSetup: './pre-setup.js', + globalTeardown: './teardown.js', + testEnvironment: './puppeteer_environment.js', + testRegex: '(/e2e.*\\.spec)\\.js', +}); diff --git a/test/e2e-ui/package.json b/test/e2e-ui/package.json new file mode 100644 index 000000000..ef1c9c021 --- /dev/null +++ b/test/e2e-ui/package.json @@ -0,0 +1,19 @@ +{ + "name": "@verdaccio/e2e-ui", + "private": true, + "version": "1.0.0", + "devDependencies": { + "@verdaccio/ui-theme": "workspace:5.0.0-alpha.1", + "@verdaccio/commons-api": "workspace:10.0.0-alpha.1", + "debug": "4.3.1", + "kleur": "^4.1.3", + "request": "^2.88.2", + "lodash": "^4.17.20", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2", + "puppeteer": "^5.5.0" + }, + "scripts": { + "test": "jest --config jest.config.e2e.js" + } +} diff --git a/test/e2e-ui/partials/pkg-protected.js b/test/e2e-ui/partials/pkg-protected.js new file mode 100644 index 000000000..5cf3a350c --- /dev/null +++ b/test/e2e-ui/partials/pkg-protected.js @@ -0,0 +1,51 @@ +const json = { + _id: 'protected-pkg', + name: 'protected-pkg', + description: '', + 'dist-tags': { + latest: '5.0.5', + }, + versions: { + '5.0.5': { + name: 'protected-pkg', + version: '5.0.5', + description: '', + main: 'index.js', + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + keywords: [], + author: { + name: 'User NPM', + email: 'user@domain.com', + }, + license: 'ISC', + dependencies: { + verdaccio: '^2.7.2', + }, + readme: '# test', + readmeFilename: 'README.md', + _id: 'protected-pkg@5.0.5', + _npmVersion: '5.5.1', + _nodeVersion: '8.7.0', + _npmUser: {}, + dist: { + integrity: + 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==', + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', + tarball: 'http://localhost:5555/protected-pkg/-/protected-pkg-5.0.5.tgz', + }, + }, + }, + readme: '# test', + _attachments: { + 'protected-pkg-5.0.5.tgz': { + content_type: 'application/octet-stream', + data: + 'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=', + length: 512, + }, + }, +}; + +module.exports = json; diff --git a/test/e2e-ui/partials/pkg-scoped.js b/test/e2e-ui/partials/pkg-scoped.js new file mode 100644 index 000000000..b0cb4a890 --- /dev/null +++ b/test/e2e-ui/partials/pkg-scoped.js @@ -0,0 +1,51 @@ +const json = { + _id: '@scope/pk1-test', + name: '@scope/pk1-test', + description: '', + 'dist-tags': { + latest: '1.0.6', + }, + versions: { + '1.0.6': { + name: '@scope/pk1-test', + version: '1.0.6', + description: '', + main: 'index.js', + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + keywords: [], + author: { + name: 'User NPM', + email: 'user@domain.com', + }, + license: 'ISC', + dependencies: { + verdaccio: '^2.7.2', + }, + readme: '# test', + readmeFilename: 'README.md', + _id: '@scope/pk1-test@1.0.6', + _npmVersion: '5.5.1', + _nodeVersion: '8.7.0', + _npmUser: {}, + dist: { + integrity: + 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==', + shasum: '2c03764f651a9f016ca0b7620421457b619151b9', + tarball: 'http://localhost:5555/@scope/pk1-test/-/@scope/pk1-test-1.0.6.tgz', + }, + }, + }, + readme: '# test', + _attachments: { + '@scope/pk1-test-1.0.6.tgz': { + content_type: 'application/octet-stream', + data: + 'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=', + length: 512, + }, + }, +}; + +module.exports = json; diff --git a/test/e2e-ui/pre-setup.js b/test/e2e-ui/pre-setup.js new file mode 100644 index 000000000..fc9e89437 --- /dev/null +++ b/test/e2e-ui/pre-setup.js @@ -0,0 +1,4 @@ +require('@babel/register')({ + extensions: ['.ts', '.js'], +}); +module.exports = require('./setup'); diff --git a/test/e2e-ui/puppeteer_environment.js b/test/e2e-ui/puppeteer_environment.js new file mode 100644 index 000000000..ced7b1a7a --- /dev/null +++ b/test/e2e-ui/puppeteer_environment.js @@ -0,0 +1,78 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const NodeEnvironment = require('jest-environment-node'); +const { yellow } = require('kleur'); +const puppeteer = require('puppeteer'); + +const VerdaccioProcess = require('./registry-launcher'); +const Server = require('./server'); + +const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); + +class VerdaccioConfig { + constructor(storagePath, configPath, domainPath, port) { + this.storagePath = storagePath; + this.configPath = configPath; + this.domainPath = domainPath; + this.port = port; + } +} + +class PuppeteerEnvironment extends NodeEnvironment { + constructor(config) { + super(config); + } + + async setup() { + const config1 = new VerdaccioConfig( + path.join(__dirname, './store-e2e'), + path.join(__dirname, './config/config-scoped-e2e.yaml'), + 'http://0.0.0.0:55558/', + 55558 + ); + const config2 = new VerdaccioConfig( + path.join(__dirname, './store-e2e'), + path.join(__dirname, './config/config-protected-e2e.yaml'), + 'http://0.0.0.0:55552/', + 55552 + ); + const server1 = new Server.default(config1.domainPath); + const server2 = new Server.default(config2.domainPath); + const process1 = new VerdaccioProcess.default(config1, server1); + const process2 = new VerdaccioProcess.default(config2, server2); + const verdaccioPath = path.normalize( + path.join(process.cwd(), '../../packages/verdaccio/debug/bootstrap.js') + ); + const fork = await process1.init(verdaccioPath); + const fork2 = await process2.init(verdaccioPath); + this.global.__VERDACCIO_E2E__ = fork[0]; + this.global.__VERDACCIO__PROTECTED_E2E__ = fork2[0]; + + console.log(yellow('Setup Test Environment.')); + await super.setup(); + const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8'); + if (!wsEndpoint) { + throw new Error('wsEndpoint not found'); + } + this.global.__SERVER__ = server1; + this.global.__SERVER_PROTECTED__ = server2; + this.global.__BROWSER__ = await puppeteer.connect({ + browserWSEndpoint: wsEndpoint, + }); + } + + async teardown() { + console.log(yellow('Teardown Test Environment.')); + await super.teardown(); + this.global.__VERDACCIO_E2E__.stop(); + this.global.__VERDACCIO__PROTECTED_E2E__.stop(); + } + + runScript(script) { + return super.runScript(script); + } +} + +module.exports = PuppeteerEnvironment; diff --git a/test/e2e-ui/registry-launcher.ts b/test/e2e-ui/registry-launcher.ts new file mode 100644 index 000000000..887bc6327 --- /dev/null +++ b/test/e2e-ui/registry-launcher.ts @@ -0,0 +1,64 @@ +import { fork } from 'child_process'; +import path from 'path'; + +import { HTTP_STATUS } from '@verdaccio/commons-api'; + +export const CREDENTIALS = { + user: 'foo', + password: 'test', +}; + +export default class VerdaccioProcess { + private bridge; + private config; + private childFork; + + public constructor(config, bridge) { + this.config = config; + this.bridge = bridge; + } + + public init(verdaccioPath) { + return new Promise((resolve, reject) => { + this._start(verdaccioPath, resolve, reject); + }); + } + + private _start(verdaccioPath: string, resolve: Function, reject: Function) { + const verdaccioRegisterWrap: string = path.join(verdaccioPath); + const childOptions = { + silent: false, + }; + + const { configPath, port } = this.config; + this.childFork = fork( + verdaccioRegisterWrap, + ['-c', configPath, '-l', port as string], + childOptions + ); + + this.childFork.on('message', (msg) => { + // verdaccio_started is a message that comes from verdaccio in debug mode that notify has been started + if ('verdaccio_started' in msg) { + this.bridge + .debug() + .status(HTTP_STATUS.OK) + .then((body) => { + this.bridge + .auth(CREDENTIALS.user, CREDENTIALS.password) + .status(HTTP_STATUS.CREATED) + .body_ok(new RegExp(CREDENTIALS.user)) + .then(() => resolve([this, body.pid]), reject); + }, reject); + } + }); + + this.childFork.on('error', (err) => reject([err, this])); + this.childFork.on('disconnect', (err) => reject([err, this])); + this.childFork.on('exit', (err) => reject([err, this])); + } + + public stop(): void { + return this.childFork.kill('SIGINT'); + } +} diff --git a/test/e2e-ui/request.ts b/test/e2e-ui/request.ts new file mode 100644 index 000000000..09247b84d --- /dev/null +++ b/test/e2e-ui/request.ts @@ -0,0 +1,137 @@ +import assert from 'assert'; + +import _ from 'lodash'; +import request from 'request'; + +const requestData = Symbol('smart_request_data'); + +export interface RequestPromise { + status(reason: any): any; + body_ok(reason: any): any; + body_error(reason: any): any; + request(reason: any): any; + response(reason: any): any; + send(reason: any): any; +} + +function injectResponse(smartObject: any, promise: Promise): Promise { + // $FlowFixMe + promise[requestData] = smartObject[requestData]; + return promise; +} + +export class PromiseAssert extends Promise implements RequestPromise { + public constructor(options: any) { + super(options); + } + + public status(expected: number) { + const selfData = this[requestData]; + + return injectResponse( + this, + this.then(function (body) { + try { + assert.equal(selfData.response.statusCode, expected); + } catch (err) { + selfData.error.message = err.message; + throw selfData.error; + } + return body; + }) + ); + } + + public body_ok(expected: any) { + const selfData = this[requestData]; + + return injectResponse( + this, + this.then(function (body) { + try { + if (_.isRegExp(expected)) { + assert(body.ok.match(expected), "'" + body.ok + "' doesn't match " + expected); + } else { + assert.equal(body.ok, expected); + } + assert.equal(body.error, null); + } catch (err) { + selfData.error.message = err.message; + throw selfData.error; + } + + return body; + }) + ); + } + + public body_error(expected: any) { + // $FlowFixMe + const selfData = this[requestData]; + + return injectResponse( + this, + this.then(function (body) { + try { + if (_.isRegExp(expected)) { + assert(body.error.match(expected), body.error + " doesn't match " + expected); + } else { + assert.equal(body.error, expected); + } + assert.equal(body.ok, null); + } catch (err) { + selfData.error.message = err.message; + throw selfData.error; + } + return body; + }) + ); + } + + public request(callback: any) { + callback(this[requestData].request); + return this; + } + + public response(cb: any) { + const selfData = this[requestData]; + + return injectResponse( + this, + this.then(function (body) { + cb(selfData.response); + return body; + }) + ); + } + + public send(data: any) { + this[requestData].request.end(data); + return this; + } +} + +function smartRequest(options: any): Promise { + const smartObject: any = {}; + + smartObject[requestData] = {}; + smartObject[requestData].error = Error(); + Error.captureStackTrace(smartObject[requestData].error, smartRequest); + + const promiseResult: Promise = new PromiseAssert(function (resolve, reject) { + // store request reference on symbol + smartObject[requestData].request = request(options, function (err, res, body) { + if (err) { + return reject(err); + } + + // store the response on symbol + smartObject[requestData].response = res; + resolve(body); + }); + }); + + return injectResponse(smartObject, promiseResult); +} + +export default smartRequest; diff --git a/test/e2e-ui/server.ts b/test/e2e-ui/server.ts new file mode 100644 index 000000000..4878995e9 --- /dev/null +++ b/test/e2e-ui/server.ts @@ -0,0 +1,276 @@ +import assert from 'assert'; + +import { HTTP_STATUS, HEADERS, API_MESSAGE } from '@verdaccio/commons-api'; +import _ from 'lodash'; + +import { CREDENTIALS } from './registry-launcher'; +import smartRequest, { RequestPromise } from './request'; + +declare class PromiseAssert extends Promise { + public constructor(options: any); +} + +const TARBALL = 'tarball-blahblah-file.name'; + +function getPackage(name, version = '0.0.0'): any { + return { + name, + version, + readme: 'this is a readme', + dist: { + shasum: 'fake', + tarball: `http://localhost:0000/${encodeURIComponent(name)}/-/${TARBALL}`, + }, + }; +} + +export interface ServerBridge { + url: string; + userAgent: string; + authstr: string; + request(options: any): typeof PromiseAssert; + auth(name: string, password: string): RequestPromise; + auth(name: string, password: string): RequestPromise; + logout(token: string): Promise; + getPackage(name: string): Promise; + putPackage(name: string, data: any): Promise; + putVersion(name: string, version: string, data: any): Promise; + getTarball(name: string, filename: string): Promise; + putTarball(name: string, filename: string, data: any): Promise; + removeTarball(name: string): Promise; + removeSingleTarball(name: string, filename: string): Promise; + addTag(name: string, tag: string, version: string): Promise; + putTarballIncomplete( + name: string, + filename: string, + data: any, + size: number, + cb: Function + ): Promise; + addPackage(name: string): Promise; + whoami(): Promise; + ping(): Promise; + debug(): RequestPromise; +} + +const TOKEN_BASIC = 'Basic'; + +function buildToken(type: string, token: string): string { + return `${_.capitalize(type)} ${token}`; +} + +const buildAuthHeader = (user, pass): string => { + return buildToken(TOKEN_BASIC, new Buffer(`${user}:${pass}`).toString('base64')); +}; + +export default class Server implements ServerBridge { + public url: string; + public userAgent: string; + public authstr: string; + + public constructor(url: string) { + this.url = url.replace(/\/$/, ''); + this.userAgent = 'node/v8.1.2 linux x64'; + this.authstr = buildAuthHeader(CREDENTIALS.user, CREDENTIALS.password); + } + + public request(options: any): any { + assert(options.uri); + const headers = options.headers || {}; + + headers.accept = headers.accept || HEADERS.JSON; + headers['user-agent'] = headers['user-agent'] || this.userAgent; + headers.authorization = headers.authorization || this.authstr; + + return smartRequest({ + url: this.url + options.uri, + method: options.method || 'GET', + headers: headers, + encoding: options.encoding, + json: _.isNil(options.json) === false ? options.json : true, + }); + } + + public auth(name: string, password: string) { + // pragma: allowlist secret + this.authstr = buildAuthHeader(name, password); + return this.request({ + uri: `/-/user/org.couchdb.user:${encodeURIComponent(name)}/-rev/undefined`, + method: 'PUT', + json: { + name, + password, + email: `${CREDENTIALS.user}@example.com`, + _id: `org.couchdb.user:${name}`, + type: 'user', + roles: [], + date: new Date(), + }, + }); + } + + public logout(token: string) { + return this.request({ + uri: `/-/user/token/${encodeURIComponent(token)}`, + method: 'DELETE', + }); + } + + public getPackage(name: string) { + return this.request({ + uri: `/${encodeURIComponent(name)}`, + method: 'GET', + }); + } + + public putPackage(name: string, data) { + if (_.isObject(data) && !Buffer.isBuffer(data)) { + data = JSON.stringify(data); + } + + return this.request({ + uri: `/${encodeURIComponent(name)}`, + method: 'PUT', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.JSON, + }, + }).send(data); + } + + public putVersion(name: string, version: string, data: any) { + if (_.isObject(data) && !Buffer.isBuffer(data)) { + data = JSON.stringify(data); + } + + return this.request({ + uri: `/${encodeURIComponent(name)}/${encodeURIComponent(version)}/-tag/latest`, + method: 'PUT', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.JSON, + }, + }).send(data); + } + + public getTarball(name: string, filename: string) { + return this.request({ + uri: `/${encodeURIComponent(name)}/-/${encodeURIComponent(filename)}`, + method: 'GET', + encoding: null, + }); + } + + public putTarball(name: string, filename: string, data: any) { + return this.request({ + uri: `/${encodeURIComponent(name)}/-/${encodeURIComponent(filename)}/whatever`, + method: 'PUT', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.OCTET_STREAM, + }, + }).send(data); + } + + public removeTarball(name: string) { + return this.request({ + uri: `/${encodeURIComponent(name)}/-rev/whatever`, + method: 'DELETE', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.JSON_CHARSET, + }, + }); + } + + public removeSingleTarball(name: string, filename: string) { + return this.request({ + uri: `/${encodeURIComponent(name)}/-/${filename}/-rev/whatever`, + method: 'DELETE', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.JSON_CHARSET, + }, + }); + } + + public addTag(name: string, tag: string, version: string) { + return this.request({ + uri: `/${encodeURIComponent(name)}/${encodeURIComponent(tag)}`, + method: 'PUT', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.JSON, + }, + }).send(JSON.stringify(version)); + } + + public putTarballIncomplete( + pkgName: string, + filename: string, + data: any, + headerContentSize: number + ): Promise { + const promise = this.request({ + uri: `/${encodeURIComponent(pkgName)}/-/${encodeURIComponent(filename)}/whatever`, + method: 'PUT', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.OCTET_STREAM, + [HEADERS.CONTENT_LENGTH]: headerContentSize, + }, + timeout: 1000, + }); + + promise.request(function (req) { + req.write(data); + // it auto abort the request + setTimeout(function () { + req.req.abort(); + }, 20); + }); + + return new Promise(function (resolve, reject) { + promise + .then(function () { + reject(Error('no error')); + }) + .catch(function (err) { + if (err.code === 'ECONNRESET') { + resolve(); + } else { + reject(err); + } + }); + }); + } + + public addPackage(name: string) { + return this.putPackage(name, getPackage(name)) + .status(HTTP_STATUS.CREATED) + .body_ok(API_MESSAGE.PKG_CREATED); + } + + public whoami() { + return this.request({ + uri: '/-/whoami', + }) + .status(HTTP_STATUS.OK) + .then(function (body) { + return body.username; + }); + } + + public ping() { + return this.request({ + uri: '/-/ping', + }) + .status(HTTP_STATUS.OK) + .then(function (body) { + return body; + }); + } + + public debug() { + return this.request({ + uri: '/-/_debug', + method: 'GET', + headers: { + [HEADERS.CONTENT_TYPE]: HEADERS.JSON, + }, + }); + } +} diff --git a/test/e2e-ui/setup.js b/test/e2e-ui/setup.js new file mode 100644 index 000000000..f5b6f26cb --- /dev/null +++ b/test/e2e-ui/setup.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { green } = require('kleur'); +const mkdirp = require('mkdirp'); +const puppeteer = require('puppeteer'); + +const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); + +module.exports = async function () { + console.log(green('Setup Puppeteer')); + const browser = await puppeteer.launch({ + headless: true, + // slowMo: 600, + // devtools: true, + args: ['--no-sandbox'], + }); + global.__BROWSER__ = browser; + mkdirp.sync(DIR); + fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint()); +}; diff --git a/test/e2e-ui/teardown.js b/test/e2e-ui/teardown.js new file mode 100644 index 000000000..eea6366b4 --- /dev/null +++ b/test/e2e-ui/teardown.js @@ -0,0 +1,13 @@ +const os = require('os'); +const path = require('path'); + +const { green } = require('kleur'); +const rimraf = require('rimraf'); + +const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); + +module.exports = async function () { + console.log(green('Teardown Puppeteer')); + await global.__BROWSER__.close(); + rimraf.sync(DIR); +}; diff --git a/website/package.json b/website/package.json index 651ec78e7..208e5a3ec 100644 --- a/website/package.json +++ b/website/package.json @@ -17,6 +17,9 @@ "babel-preset-gatsby": "^0.4.12", "classnames": "^2.2.6", "emotion-theming": "10.0.27", + "prop-types": "15.7.2", + "react": "16.13.1", + "react-dom": "16.13.1", "event-source-polyfill": "^1.0.17", "fontsource-roboto": "^2.2.6", "gatsby": "^2.24.51", @@ -40,10 +43,7 @@ "lisan": "^0.1.1", "mitt": "2.1.0", "prismjs": "^1.21.0", - "prop-types": "15.7.2", "query-string": "^6.13.1", - "react": "16.13.1", - "react-dom": "16.13.1", "react-error-overlay": "^6.0.7", "react-helmet": "5.2.1", "react-twitter-widgets": "^1.9.5",