refactor: enable e2e for UI (#2036)

* refactor: enable e2e for UI

* refactor: enable e2e for UI

* refactor: enable e2e for UI

* refactor: enable e2e for UI

* refactor: enable e2e for UI

* refactor: enable e2e for UI
This commit is contained in:
Juan Picado 2020-12-22 13:10:20 +01:00
parent e9e4552658
commit 821bd776f3
28 changed files with 1103 additions and 29 deletions

View File

@ -1,6 +1,18 @@
name: E2E UI 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: jobs:
ci: ci:
@ -16,12 +28,18 @@ jobs:
steps: steps:
- uses: actions/checkout@v2.3.1 - uses: actions/checkout@v2.3.1
- name: Use Node ${{ matrix.node_version }} - name: Use Node ${{ matrix.node_version }}
uses: actions/setup-node@v2.1.5 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node_version }} node_version: ${{ matrix.node_version }}
- name: Install pnpm
run: npm i -g pnpm@latest
- name: Install - name: Install
run: yarn install --immutable run: pnpm recursive install
- name: Clean
run: pnpm clean
- name: Build - name: Build
run: yarn code:build run: pnpm build
- name: Test UI - name: Test UI
run: yarn run test:e2e run: pnpm test:e2e:ui
env:
DEBUG: verdaccio:e2e*

View File

@ -1,4 +1,4 @@
name: CI E2E name: E2E CLI
on: on:
push: push:
@ -7,12 +7,12 @@ on:
- 'changeset-release/5.x' - 'changeset-release/5.x'
pull_request: pull_request:
paths: paths:
- .changeset/**
- .github/workflows/ci.yml - .github/workflows/ci.yml
- 'packages/**' - 'packages/**'
- 'jest/**' - 'jest/**'
- 'package.json' - 'package.json'
- 'pnpm-workspace.yaml' - 'pnpm-workspace.yaml'
- 'test/**'
jobs: jobs:
ci: ci:
@ -39,7 +39,7 @@ jobs:
run: pnpm clean run: pnpm clean
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Test - name: Test CLI
run: pnpm test:e2e:cli run: pnpm test:e2e:cli
env: env:
DEBUG: verdaccio:e2e* DEBUG: verdaccio:e2e*

View File

@ -5,7 +5,11 @@ on:
branches-ignore: branches-ignore:
- 5.x - 5.x
- 'changeset-release/5.x' - 'changeset-release/5.x'
- 'dev/**'
pull_request: pull_request:
paths:
- .github/workflows/ci.yml
- 'packages/**'
schedule: schedule:
- cron: '0 2 * * 4' - cron: '0 2 * * 4'

View File

@ -13,6 +13,7 @@
CHANGELOG.md CHANGELOG.md
CONTRIBUTORS.md CONTRIBUTORS.md
node_modules/ node_modules/
**/coverage/**
**/static/*.js **/static/*.js
**/build/*.js **/build/*.js
packages/core/local-storage/_storage/** packages/core/local-storage/_storage/**

View File

@ -128,6 +128,7 @@
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\"", "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
"test": "pnpm recursive test --filter ./packages", "test": "pnpm recursive test --filter ./packages",
"test:e2e:cli": "pnpm test --filter ...@verdaccio/e2e-cli", "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": "concurrently --kill-others \"pnpm start:server\" \"pnpm start:web\"",
"start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000", "start:server": "node packages/verdaccio/debug/bootstrap.js --listen 8000",
"start:web": "pnpm start --filter ...@verdaccio/ui-theme", "start:web": "pnpm start --filter ...@verdaccio/ui-theme",

View File

@ -25,7 +25,6 @@
"@testing-library/dom": "^7.29.0", "@testing-library/dom": "^7.29.0",
"@testing-library/jest-dom": "^5.11.6", "@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "10.4.9", "@testing-library/react": "10.4.9",
"@verdaccio/commons-api": "workspace:10.0.0-alpha.1",
"@verdaccio/node-api": "workspace:5.0.0-alpha.1", "@verdaccio/node-api": "workspace:5.0.0-alpha.1",
"autosuggest-highlight": "3.1.1", "autosuggest-highlight": "3.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-dynamic-import-node": "^2.3.3",
@ -56,11 +55,7 @@
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"optimize-css-assets-webpack-plugin": "^5.0.4", "optimize-css-assets-webpack-plugin": "^5.0.4",
"ora": "4.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-autosuggest": "10.0.2",
"react-dom": "16.13.1",
"react-hook-form": "3.29.4", "react-hook-form": "3.29.4",
"react-hot-loader": "4.12.21", "react-hot-loader": "4.12.21",
"react-i18next": "^11.8.3", "react-i18next": "^11.8.3",
@ -70,7 +65,10 @@
"request": "2.88.2", "request": "2.88.2",
"resolve-url-loader": "3.1.1", "resolve-url-loader": "3.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"source-map-loader": "1.1.0", "source-map-loader": "1.1.0",
"prop-types": "15.7.2",
"standard-version": "9.0.0", "standard-version": "9.0.0",
"style-loader": "1.2.1", "style-loader": "1.2.1",
"stylelint": "13.7.2", "stylelint": "13.7.2",
@ -124,7 +122,6 @@
"type-check": "tsc --noEmit -p tsconfig.build.json", "type-check": "tsc --noEmit -p tsconfig.build.json",
"start": "babel-node tools/dev.server.js", "start": "babel-node tools/dev.server.js",
"test:clean": "jest --clearCache", "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": "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:update-snapshot": "yarn run test -- -u",
"test:size": "bundlesize", "test:size": "bundlesize",

View File

@ -640,7 +640,6 @@ importers:
'@testing-library/dom': 7.29.0 '@testing-library/dom': 7.29.0
'@testing-library/jest-dom': 5.11.6 '@testing-library/jest-dom': 5.11.6
'@testing-library/react': 10.4.9_react-dom@16.13.1+react@16.13.1 '@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' '@verdaccio/node-api': 'link:../../node-api'
autosuggest-highlight: 3.1.1 autosuggest-highlight: 3.1.1
babel-loader: 8.2.2_webpack@5.10.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 optimize-css-assets-webpack-plugin: 5.0.4_webpack@5.10.1
ora: 4.0.4 ora: 4.0.4
prop-types: 15.7.2 prop-types: 15.7.2
puppeteer: 5.3.1
react: 16.13.1 react: 16.13.1
react-autosuggest: 10.0.2_react@16.13.1 react-autosuggest: 10.0.2_react@16.13.1
react-dom: 16.13.1_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/dom': ^7.29.0
'@testing-library/jest-dom': ^5.11.6 '@testing-library/jest-dom': ^5.11.6
'@testing-library/react': 10.4.9 '@testing-library/react': 10.4.9
'@verdaccio/commons-api': 'workspace:10.0.0-alpha.1'
'@verdaccio/node-api': 'workspace:5.0.0-alpha.1' '@verdaccio/node-api': 'workspace:5.0.0-alpha.1'
autosuggest-highlight: 3.1.1 autosuggest-highlight: 3.1.1
babel-loader: ^8.2.2 babel-loader: ^8.2.2
@ -752,7 +749,6 @@ importers:
optimize-css-assets-webpack-plugin: ^5.0.4 optimize-css-assets-webpack-plugin: ^5.0.4
ora: 4.0.4 ora: 4.0.4
prop-types: 15.7.2 prop-types: 15.7.2
puppeteer: 5.3.1
react: 16.13.1 react: 16.13.1
react-autosuggest: 10.0.2 react-autosuggest: 10.0.2
react-dom: 16.13.1 react-dom: 16.13.1
@ -982,6 +978,27 @@ importers:
request: ^2.88.2 request: ^2.88.2
semver: ^7.3.4 semver: ^7.3.4
yarn: 1.22.10 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: website:
dependencies: dependencies:
'@emotion/core': 10.0.28_react@16.13.1 '@emotion/core': 10.0.28_react@16.13.1
@ -11291,10 +11308,10 @@ packages:
dev: false dev: false
resolution: resolution:
integrity: sha512-7/nIzKdQ8y2K0imjIP7dyg2GJ2h38Ps6VOMXWZHIarNDV3p6mTXyEugKFnkmsZ2DD58JEG34ILyVb3qdOMmP9w== integrity: sha512-7/nIzKdQ8y2K0imjIP7dyg2GJ2h38Ps6VOMXWZHIarNDV3p6mTXyEugKFnkmsZ2DD58JEG34ILyVb3qdOMmP9w==
/devtools-protocol/0.0.799653: /devtools-protocol/0.0.818844:
dev: true dev: true
resolution: resolution:
integrity: sha512-t1CcaZbvm8pOlikqrsIM9GOa7Ipp07+4h/q9u0JXBWjPCjHdBl9KkddX87Vv9vBHoBGtwV79sYQNGnQM6iS5gg== integrity: sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==
/diacritic/0.0.2: /diacritic/0.0.2:
dev: true dev: true
resolution: resolution:
@ -17586,6 +17603,12 @@ packages:
node: '>=6' node: '>=6'
resolution: resolution:
integrity: sha512-FGCCxczbrZuF5CtMeO0xfnjhzkVZSXfcWK90IPLucDWZwskrpYN7pmRIgvd8muU0mrPrzy4A2RBGuwCjLHI+nw== 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: /known-css-properties/0.19.0:
dev: true dev: true
resolution: resolution:
@ -21506,12 +21529,13 @@ packages:
node: '>=8' node: '>=8'
resolution: resolution:
integrity: sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== integrity: sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==
/puppeteer/5.3.1: /puppeteer/5.5.0:
dependencies: dependencies:
debug: 4.3.1 debug: 4.3.1
devtools-protocol: 0.0.799653 devtools-protocol: 0.0.818844
extract-zip: 2.0.1 extract-zip: 2.0.1
https-proxy-agent: 4.0.0 https-proxy-agent: 4.0.0
node-fetch: 2.6.1
pkg-dir: 4.2.0 pkg-dir: 4.2.0
progress: 2.0.3 progress: 2.0.3
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
@ -21524,7 +21548,7 @@ packages:
node: '>=10.18.1' node: '>=10.18.1'
requiresBuild: true requiresBuild: true
resolution: resolution:
integrity: sha512-YTM1RaBeYrj6n7IlRXRYLqJHF+GM7tasbvrNFx6w1S16G76NrPq7oYFKLDO+BQsXNtS8kW2GxWCXjIMPvfDyaQ== integrity: sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==
/q/1.5.1: /q/1.5.1:
engines: engines:
node: '>=0.6.0' node: '>=0.6.0'

View File

@ -3,4 +3,4 @@ packages:
- packages/core/* - packages/core/*
- packages/plugins/* - packages/plugins/*
- website - website
- test/e2e-cli - test/e2e-*

6
test/README.md Normal file
View File

@ -0,0 +1,6 @@
# E2E Testing
This folder is composed of two strategies:
- E2E for CLI
- E2E for the UI (puppeteer)

View File

@ -1,4 +1,4 @@
# E2E Testing # E2E CLI Testing
## What is included on these test? ## What is included on these test?

3
test/e2e-ui/.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../../.babelrc"
}

15
test/e2e-ui/.eslintrc Normal file
View File

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

9
test/e2e-ui/README.md Normal file
View File

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

View File

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

View File

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

213
test/e2e-ui/e2e.spec.js Normal file
View File

@ -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...");
});
});

View File

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

19
test/e2e-ui/package.json Normal file
View File

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

View File

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

View File

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

4
test/e2e-ui/pre-setup.js Normal file
View File

@ -0,0 +1,4 @@
require('@babel/register')({
extensions: ['.ts', '.js'],
});
module.exports = require('./setup');

View File

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

View File

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

137
test/e2e-ui/request.ts Normal file
View File

@ -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<any>): Promise<any> {
// $FlowFixMe
promise[requestData] = smartObject[requestData];
return promise;
}
export class PromiseAssert extends Promise<any> 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<any> {
const smartObject: any = {};
smartObject[requestData] = {};
smartObject[requestData].error = Error();
Error.captureStackTrace(smartObject[requestData].error, smartRequest);
const promiseResult: Promise<any> = 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;

276
test/e2e-ui/server.ts Normal file
View File

@ -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<RequestPromise> extends Promise<any> {
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<any>;
getPackage(name: string): Promise<any>;
putPackage(name: string, data: any): Promise<any>;
putVersion(name: string, version: string, data: any): Promise<any>;
getTarball(name: string, filename: string): Promise<any>;
putTarball(name: string, filename: string, data: any): Promise<any>;
removeTarball(name: string): Promise<any>;
removeSingleTarball(name: string, filename: string): Promise<any>;
addTag(name: string, tag: string, version: string): Promise<any>;
putTarballIncomplete(
name: string,
filename: string,
data: any,
size: number,
cb: Function
): Promise<any>;
addPackage(name: string): Promise<any>;
whoami(): Promise<any>;
ping(): Promise<any>;
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<any> {
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,
},
});
}
}

22
test/e2e-ui/setup.js Normal file
View File

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

13
test/e2e-ui/teardown.js Normal file
View File

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

View File

@ -17,6 +17,9 @@
"babel-preset-gatsby": "^0.4.12", "babel-preset-gatsby": "^0.4.12",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"emotion-theming": "10.0.27", "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", "event-source-polyfill": "^1.0.17",
"fontsource-roboto": "^2.2.6", "fontsource-roboto": "^2.2.6",
"gatsby": "^2.24.51", "gatsby": "^2.24.51",
@ -40,10 +43,7 @@
"lisan": "^0.1.1", "lisan": "^0.1.1",
"mitt": "2.1.0", "mitt": "2.1.0",
"prismjs": "^1.21.0", "prismjs": "^1.21.0",
"prop-types": "15.7.2",
"query-string": "^6.13.1", "query-string": "^6.13.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-error-overlay": "^6.0.7", "react-error-overlay": "^6.0.7",
"react-helmet": "5.2.1", "react-helmet": "5.2.1",
"react-twitter-widgets": "^1.9.5", "react-twitter-widgets": "^1.9.5",