feat: bundle assets with content (#2958)
* fix(media_folder_relative): use collection name in unpublished entry * refactor: pass arguments as object to AssetProxy ctor * feat: support media folders per collection * feat: resolve media files path based on entry path * fix: asset public path resolving * refactor: introduce typescript for AssetProxy * refactor: code cleanup * refactor(asset-proxy): add tests,switch to typescript,extract arguments * refactor: typescript for editorialWorkflow * refactor: add typescript for media library actions * refactor: fix type error on map set * refactor: move locale selector into reducer * refactor: add typescript for entries actions * refactor: remove duplication between asset store and media lib * feat: load assets from backend using API * refactor(github): add typescript, cache media files * fix: don't load media URL if already loaded * feat: add media folder config to collection * fix: load assets from API when not in UI state * feat: load entry media files when opening media library * fix: editorial workflow draft media files bug fixes * test(unit): fix unit tests * fix: editor control losing focus * style: add eslint object-shorthand rule * test(cypress): re-record mock data * fix: fix non github backends, large media * test: uncomment only in tests * fix(backend-test): add missing displayURL property * test(e2e): add media library tests * test(e2e): enable visual testing * test(e2e): add github backend media library tests * test(e2e): add git-gateway large media tests * chore: post rebase fixes * test: fix tests * test: fix tests * test(cypress): fix tests * docs: add media_folder docs * test(e2e): add media library delete test * test(e2e): try and fix image comparison on CI * ci: reduce test machines from 9 to 8 * test: add reducers and selectors unit tests * test(e2e): disable visual regression testing for now * test: add getAsset unit tests * refactor: use Asset class component instead of hooks * build: don't inline source maps * test: add more media path tests
33
.eslintrc.js
@ -28,6 +28,8 @@ module.exports = {
|
||||
'emotion/no-vanilla': 'error',
|
||||
'emotion/import-from-emotion': 'error',
|
||||
'emotion/styled-import': 'error',
|
||||
'require-atomic-updates': [0],
|
||||
'object-shorthand': ['error', 'always'],
|
||||
},
|
||||
plugins: ['babel', 'emotion', 'cypress'],
|
||||
settings: {
|
||||
@ -35,4 +37,35 @@ module.exports = {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:cypress/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:import/typescript',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'require-atomic-updates': [0],
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
'error',
|
||||
{ functions: false, classes: true, variables: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
6
.github/workflows/nodejs.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
||||
name: dev-test-website-node-${{ matrix.node-version }}
|
||||
path: dev-test
|
||||
|
||||
# non forked workflow (has acceess to build secrets)
|
||||
# non forked workflow (has access to build secrets)
|
||||
e2e-with-cypress-record:
|
||||
needs: build
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
machine: [1, 2, 3, 4, 5]
|
||||
machine: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@ -85,7 +85,7 @@ jobs:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
# forked workflow (no acceess to build secrets)
|
||||
# forked workflow (no access to build secrets)
|
||||
e2e-no-cypress-record:
|
||||
needs: build
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
|
1
.gitignore
vendored
@ -12,6 +12,7 @@ manifest.yml
|
||||
website/data/contributors.json
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
__diff_output__
|
||||
/coverage/
|
||||
.cache
|
||||
*.log
|
||||
|
@ -50,6 +50,7 @@ const defaultPlugins = [
|
||||
localforage: 'localforage',
|
||||
redux: 'redux',
|
||||
},
|
||||
extensions: ['.js', '.jsx', '.es', '.es6', '.mjs', '.ts', '.tsx'],
|
||||
}
|
||||
: {
|
||||
root: path.join(__dirname, 'packages/netlify-cms-core/src/components'),
|
||||
@ -68,6 +69,7 @@ const defaultPlugins = [
|
||||
localforage: 'localforage',
|
||||
redux: 'redux',
|
||||
},
|
||||
extensions: ['.js', '.jsx', '.es', '.es6', '.mjs', '.ts', '.tsx'],
|
||||
},
|
||||
],
|
||||
];
|
||||
@ -82,6 +84,7 @@ const presets = () => {
|
||||
autoLabel: true,
|
||||
},
|
||||
],
|
||||
'@babel/typescript',
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
const babelJest = require('babel-jest');
|
||||
const babelConfig = require('./babel.config.js');
|
||||
|
||||
module.exports = babelJest.createTransformer(babelConfig);
|
BIN
cypress/fixtures/media/netlify.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
@ -1,21 +1,19 @@
|
||||
const backend = 'github';
|
||||
|
||||
export const before = (taskResult, options) => {
|
||||
Cypress.config('taskTimeout', 5 * 60 * 1000);
|
||||
export const before = (taskResult, options, backend = 'github') => {
|
||||
Cypress.config('taskTimeout', 7 * 60 * 1000);
|
||||
cy.task('setupBackend', { backend, options }).then(data => {
|
||||
taskResult.data = data;
|
||||
Cypress.config('defaultCommandTimeout', data.mockResponses ? 5 * 1000 : 1 * 60 * 1000);
|
||||
});
|
||||
};
|
||||
|
||||
export const after = taskResult => {
|
||||
export const after = (taskResult, backend = 'github') => {
|
||||
cy.task('teardownBackend', {
|
||||
backend,
|
||||
...taskResult.data,
|
||||
});
|
||||
};
|
||||
|
||||
export const beforeEach = taskResult => {
|
||||
export const beforeEach = (taskResult, backend = 'github') => {
|
||||
const spec = Cypress.mocha.getRunner().suite.ctx.currentTest.parent.title;
|
||||
const testName = Cypress.mocha.getRunner().suite.ctx.currentTest.title;
|
||||
cy.task('setupBackendTest', {
|
||||
@ -34,7 +32,7 @@ export const beforeEach = taskResult => {
|
||||
return cy.clock(0, ['Date']);
|
||||
};
|
||||
|
||||
export const afterEach = taskResult => {
|
||||
export const afterEach = (taskResult, backend = 'github') => {
|
||||
const spec = Cypress.mocha.getRunner().suite.ctx.currentTest.parent.title;
|
||||
const testName = Cypress.mocha.getRunner().suite.ctx.currentTest.title;
|
||||
|
||||
|
169
cypress/integration/media/media_library.js
Normal file
@ -0,0 +1,169 @@
|
||||
import path from 'path';
|
||||
import '../../utils/dismiss-local-backup';
|
||||
import {
|
||||
login,
|
||||
goToMediaLibrary,
|
||||
newPost,
|
||||
populateEntry,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
goToEntry,
|
||||
goToCollections,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus } from '../../utils/constants';
|
||||
|
||||
function uploadMediaFile() {
|
||||
assertNoImagesInLibrary();
|
||||
|
||||
const fixture = 'media/netlify.png';
|
||||
cy.fixture(fixture).then(fileContent => {
|
||||
cy.get('input[type="file"]').upload({
|
||||
fileContent,
|
||||
fileName: path.basename(fixture),
|
||||
mimeType: 'image/png',
|
||||
});
|
||||
});
|
||||
|
||||
cy.contains('span', 'Uploading...').should('not.exist');
|
||||
|
||||
assertImagesInLibrary();
|
||||
}
|
||||
|
||||
function assertImagesInLibrary() {
|
||||
cy.get('img[class*="CardImage"]').should('exist');
|
||||
}
|
||||
|
||||
function assertNoImagesInLibrary() {
|
||||
cy.get('img[class*="CardImage"]').should('not.exist');
|
||||
}
|
||||
|
||||
function deleteImage() {
|
||||
cy.get('img[class*="CardImage"]').click();
|
||||
cy.contains('button', 'Delete selected').click();
|
||||
assertNoImagesInLibrary();
|
||||
}
|
||||
|
||||
function chooseSelectedMediaFile() {
|
||||
cy.contains('button', 'Choose selected').click();
|
||||
}
|
||||
|
||||
function chooseAnImage() {
|
||||
cy.contains('button', 'Choose an image').click();
|
||||
}
|
||||
|
||||
function waitForEntryToLoad() {
|
||||
cy.contains('div', 'Loading entry...').should('not.exist');
|
||||
}
|
||||
|
||||
function matchImageSnapshot() {
|
||||
// cy.matchImageSnapshot();
|
||||
}
|
||||
|
||||
function newPostAndUploadImage() {
|
||||
newPost();
|
||||
chooseAnImage();
|
||||
uploadMediaFile();
|
||||
}
|
||||
|
||||
function newPostWithImage(entry) {
|
||||
newPostAndUploadImage();
|
||||
chooseSelectedMediaFile();
|
||||
populateEntry(entry);
|
||||
waitForEntryToLoad();
|
||||
}
|
||||
|
||||
function publishPostWithImage(entry) {
|
||||
newPostWithImage(entry);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry);
|
||||
}
|
||||
|
||||
function closeMediaLibrary() {
|
||||
cy.get('button[class*="CloseButton"]').click();
|
||||
}
|
||||
|
||||
function switchToGridView() {
|
||||
cy.get('div[class*="ViewControls"]').within(() => {
|
||||
cy.get('button')
|
||||
.last()
|
||||
.click();
|
||||
});
|
||||
}
|
||||
|
||||
function assertGridEntryImage(entry) {
|
||||
cy.contains('li', entry.title).within(() => {
|
||||
cy.get('div[class*="CardImage"]').should('be.visible');
|
||||
});
|
||||
}
|
||||
|
||||
export default function({ entries, getUser }) {
|
||||
beforeEach(() => {
|
||||
login(getUser && getUser());
|
||||
});
|
||||
|
||||
it('can upload image from global media library', () => {
|
||||
goToMediaLibrary();
|
||||
uploadMediaFile();
|
||||
matchImageSnapshot();
|
||||
closeMediaLibrary();
|
||||
});
|
||||
|
||||
it('can delete image from global media library', () => {
|
||||
goToMediaLibrary();
|
||||
uploadMediaFile();
|
||||
closeMediaLibrary();
|
||||
goToMediaLibrary();
|
||||
deleteImage();
|
||||
matchImageSnapshot();
|
||||
closeMediaLibrary();
|
||||
});
|
||||
|
||||
it('can upload image from entry media library', () => {
|
||||
newPostAndUploadImage();
|
||||
matchImageSnapshot();
|
||||
closeMediaLibrary();
|
||||
exitEditor();
|
||||
});
|
||||
|
||||
it('can save entry with image', () => {
|
||||
newPostWithImage(entries[0]);
|
||||
matchImageSnapshot();
|
||||
exitEditor();
|
||||
});
|
||||
|
||||
it('can publish entry with image', () => {
|
||||
publishPostWithImage(entries[0]);
|
||||
goToEntry(entries[0]);
|
||||
waitForEntryToLoad();
|
||||
matchImageSnapshot();
|
||||
});
|
||||
|
||||
it('should not show draft entry image in global media library', () => {
|
||||
newPostWithImage(entries[0]);
|
||||
exitEditor();
|
||||
goToMediaLibrary();
|
||||
assertNoImagesInLibrary();
|
||||
matchImageSnapshot();
|
||||
});
|
||||
|
||||
it('should show published entry image in global media library', () => {
|
||||
publishPostWithImage(entries[0]);
|
||||
cy.clock().tick();
|
||||
goToMediaLibrary();
|
||||
assertImagesInLibrary();
|
||||
matchImageSnapshot();
|
||||
});
|
||||
|
||||
it('should show published entry image in grid view', () => {
|
||||
publishPostWithImage(entries[0]);
|
||||
goToCollections();
|
||||
switchToGridView();
|
||||
assertGridEntryImage(entries[0]);
|
||||
|
||||
matchImageSnapshot();
|
||||
});
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import fixture from './media/media_library';
|
||||
import { entry1 } from './github/entries';
|
||||
import * as specUtils from './github/spec_utils';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
|
||||
describe('Git Gateway Backend Media Library - Large Media', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, {}, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
import fixture from './media/media_library';
|
||||
import { entry1 } from './github/entries';
|
||||
import * as specUtils from './github/spec_utils';
|
||||
|
||||
describe('GitHub Backend Media Library - GraphQL API', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { use_graphql: true });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
import fixture from './media/media_library';
|
||||
import { entry1 } from './github/entries';
|
||||
import * as specUtils from './github/spec_utils';
|
||||
|
||||
describe('GitHub Backend Media Library - REST API', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { use_graphql: false });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
21
cypress/integration/media_library_spec_test_backend.js
Normal file
@ -0,0 +1,21 @@
|
||||
import fixture from './media/media_library';
|
||||
|
||||
const entries = [
|
||||
{
|
||||
title: 'first title',
|
||||
body: 'first body',
|
||||
},
|
||||
];
|
||||
|
||||
describe('Test Backend Media Library', () => {
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
fixture({ entries });
|
||||
});
|
250
cypress/plugins/gitGateway.js
Normal file
@ -0,0 +1,250 @@
|
||||
const fetch = require('node-fetch');
|
||||
const {
|
||||
getGitClient,
|
||||
transformRecordedData,
|
||||
setupGitHub,
|
||||
teardownGitHub,
|
||||
setupGitHubTest,
|
||||
teardownGitHubTest,
|
||||
} = require('./github');
|
||||
|
||||
function getEnvs() {
|
||||
const {
|
||||
NETLIFY_API_TOKEN: netlifyApiToken,
|
||||
GITHUB_REPO_TOKEN: githubToken,
|
||||
NETLIFY_INSTALLATION_ID: installationId,
|
||||
} = process.env;
|
||||
if (!netlifyApiToken) {
|
||||
throw new Error(
|
||||
'Please set NETLIFY_API_TOKEN, GITHUB_REPO_TOKEN, NETLIFY_INSTALLATION_ID environment variables',
|
||||
);
|
||||
}
|
||||
return { netlifyApiToken, githubToken, installationId };
|
||||
}
|
||||
|
||||
const apiRoot = 'https://api.netlify.com/api/v1/';
|
||||
|
||||
async function get(netlifyApiToken, path) {
|
||||
const response = await fetch(`${apiRoot}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${netlifyApiToken}`,
|
||||
},
|
||||
}).then(res => res.json());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function post(netlifyApiToken, path, payload) {
|
||||
const response = await fetch(`${apiRoot}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${netlifyApiToken}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
}).then(res => res.json());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function del(netlifyApiToken, path) {
|
||||
const response = await fetch(`${apiRoot}${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${netlifyApiToken}`,
|
||||
},
|
||||
}).then(res => res.text());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function createSite(netlifyApiToken, payload) {
|
||||
return post(netlifyApiToken, 'sites', payload);
|
||||
}
|
||||
|
||||
async function enableIdentity(netlifyApiToken, siteId) {
|
||||
return post(netlifyApiToken, `sites/${siteId}/identity`, {});
|
||||
}
|
||||
|
||||
async function enableGitGateway(netlifyApiToken, siteId, githubToken, repo) {
|
||||
return post(netlifyApiToken, `sites/${siteId}/services/git/instances`, {
|
||||
github: {
|
||||
repo,
|
||||
access_token: githubToken,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function enableLargeMedia(netlifyApiToken, siteId) {
|
||||
return post(netlifyApiToken, `sites/${siteId}/services/large-media/instances`, {});
|
||||
}
|
||||
|
||||
async function waitForDeploys(netlifyApiToken, siteId) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const deploys = await get(netlifyApiToken, `sites/${siteId}/deploys`);
|
||||
if (deploys.some(deploy => deploy.state === 'ready')) {
|
||||
console.log('Deploy finished for site:', siteId);
|
||||
return;
|
||||
}
|
||||
console.log('Waiting on deploy of site:', siteId);
|
||||
await new Promise(resolve => setTimeout(resolve, 30 * 1000));
|
||||
}
|
||||
console.log('Timed out waiting on deploy of site:', siteId);
|
||||
}
|
||||
|
||||
async function createUser(netlifyApiToken, siteUrl, email, password) {
|
||||
const response = await fetch(`${siteUrl}/.netlify/functions/create-user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${netlifyApiToken}`,
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('User created successfully');
|
||||
} else {
|
||||
throw new Error('Failed to create user');
|
||||
}
|
||||
}
|
||||
|
||||
const netlifySiteURL = 'https://fake-site-url.netlify.com/';
|
||||
const email = 'netlifyCMS@netlify.com';
|
||||
const password = '12345678';
|
||||
const backendName = 'git-gateway';
|
||||
|
||||
async function setupGitGateway(options) {
|
||||
const result = await setupGitHub(options);
|
||||
|
||||
if (process.env.RECORD_FIXTURES) {
|
||||
const { netlifyApiToken, githubToken, installationId } = getEnvs();
|
||||
|
||||
console.log('Creating Netlify Site');
|
||||
const { site_id, ssl_url } = await createSite(netlifyApiToken, {
|
||||
repo: {
|
||||
provider: 'github',
|
||||
installation_id: installationId,
|
||||
repo: `${result.owner}/${result.repo}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Enabling identity for site:', site_id);
|
||||
await enableIdentity(netlifyApiToken, site_id);
|
||||
|
||||
console.log('Enabling git gateway for site:', site_id);
|
||||
await enableGitGateway(netlifyApiToken, site_id, githubToken, `${result.owner}/${result.repo}`);
|
||||
|
||||
console.log('Enabling large media for site:', site_id);
|
||||
await enableLargeMedia(netlifyApiToken, site_id);
|
||||
|
||||
const git = getGitClient(result.tempDir);
|
||||
await git.raw([
|
||||
'config',
|
||||
'-f',
|
||||
'.lfsconfig',
|
||||
'lfs.url',
|
||||
`https://${site_id}.netlify.com/.netlify/large-media`,
|
||||
]);
|
||||
await git.addConfig('commit.gpgsign', 'false');
|
||||
await git.add('.lfsconfig');
|
||||
await git.commit('add .lfsconfig');
|
||||
await git.push('origin', 'master');
|
||||
|
||||
await waitForDeploys(netlifyApiToken, site_id);
|
||||
console.log('Creating user for site:', site_id, 'with email:', email);
|
||||
|
||||
try {
|
||||
await createUser(netlifyApiToken, ssl_url, email, password);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
user: {
|
||||
...result.user,
|
||||
backendName,
|
||||
netlifySiteURL: ssl_url,
|
||||
email,
|
||||
password,
|
||||
},
|
||||
site_id,
|
||||
ssl_url,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...result,
|
||||
user: {
|
||||
...result.user,
|
||||
backendName,
|
||||
netlifySiteURL,
|
||||
email,
|
||||
password,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function teardownGitGateway(taskData) {
|
||||
if (process.env.RECORD_FIXTURES) {
|
||||
const { netlifyApiToken } = getEnvs();
|
||||
const { site_id } = taskData;
|
||||
console.log('Deleting Netlify site:', site_id);
|
||||
await del(netlifyApiToken, `sites/${site_id}`);
|
||||
|
||||
const result = await teardownGitHub(taskData);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setupGitGatewayTest(taskData) {
|
||||
if (process.env.RECORD_FIXTURES) {
|
||||
const result = await setupGitHubTest(taskData);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function teardownGitGatewayTest(taskData) {
|
||||
if (process.env.RECORD_FIXTURES) {
|
||||
const options = {
|
||||
transformRecordedData: (expectation, toSanitize) => {
|
||||
const result = transformRecordedData(expectation, toSanitize);
|
||||
|
||||
const { httpRequest, httpResponse } = expectation;
|
||||
|
||||
if (
|
||||
httpResponse.body &&
|
||||
httpResponse.body.string &&
|
||||
httpRequest.path === '/.netlify/identity/token'
|
||||
) {
|
||||
let responseBody = httpResponse.body.string;
|
||||
const parsed = JSON.parse(responseBody);
|
||||
parsed.access_token = 'access_token';
|
||||
parsed.refresh_token = 'refresh_token';
|
||||
responseBody = JSON.stringify(parsed);
|
||||
return { ...result, response: responseBody };
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
},
|
||||
};
|
||||
const result = await teardownGitHubTest(taskData, options);
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupGitGateway,
|
||||
teardownGitGateway,
|
||||
setupGitGatewayTest,
|
||||
teardownGitGatewayTest,
|
||||
};
|
@ -53,6 +53,11 @@ function getEnvs() {
|
||||
return { owner, repo, token, forkOwner, forkToken };
|
||||
}
|
||||
|
||||
function getGitClient(repoDir) {
|
||||
const git = simpleGit(repoDir).env({ ...process.env, GIT_SSH_COMMAND, GIT_SSL_NO_VERIFY });
|
||||
return git;
|
||||
}
|
||||
|
||||
async function prepareTestGitHubRepo() {
|
||||
const { owner, repo, token } = getEnvs();
|
||||
|
||||
@ -71,13 +76,13 @@ async function prepareTestGitHubRepo() {
|
||||
|
||||
const tempDir = path.join('.temp', testRepoName);
|
||||
await fs.remove(tempDir);
|
||||
let git = simpleGit().env({ ...process.env, GIT_SSH_COMMAND, GIT_SSL_NO_VERIFY });
|
||||
let git = getGitClient();
|
||||
|
||||
const repoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||
|
||||
console.log('Cloning repository', repoUrl);
|
||||
await git.clone(repoUrl, tempDir);
|
||||
git = simpleGit(tempDir).env({ ...process.env, GIT_SSH_COMMAND, GIT_SSL_NO_VERIFY });
|
||||
git = getGitClient(tempDir);
|
||||
|
||||
console.log('Pushing to new repository', testRepoName);
|
||||
|
||||
@ -138,7 +143,7 @@ async function deleteRepositories({ owner, repo, tempDir }) {
|
||||
}
|
||||
|
||||
async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
console.log('Resetting origin repo:', `${owner}/repo`);
|
||||
console.log('Resetting origin repo:', `${owner}/${repo}`);
|
||||
const { token } = getEnvs();
|
||||
const client = getGitHubClient(token);
|
||||
|
||||
@ -155,6 +160,7 @@ async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
state: 'closed',
|
||||
}),
|
||||
),
|
||||
);
|
||||
@ -174,9 +180,9 @@ async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
);
|
||||
|
||||
console.log('Resetting master');
|
||||
const git = simpleGit(tempDir).env({ ...process.env, GIT_SSH_COMMAND, GIT_SSL_NO_VERIFY });
|
||||
const git = getGitClient(tempDir);
|
||||
await git.push(['--force', 'origin', 'master']);
|
||||
console.log('Done resetting origin repo:', `${owner}/repo`);
|
||||
console.log('Done resetting origin repo:', `${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async function resetForkedRepo({ repo }) {
|
||||
@ -199,7 +205,7 @@ async function resetForkedRepo({ repo }) {
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.log('Done resetting forked repo:', `${forkOwner}/repo`);
|
||||
console.log('Done resetting forked repo:', `${forkOwner}/${repo}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -306,18 +312,41 @@ const sanitizeString = (
|
||||
return replaced;
|
||||
};
|
||||
|
||||
const HEADERS_TO_IGNORE = [
|
||||
'Date',
|
||||
'X-RateLimit-Remaining',
|
||||
'X-RateLimit-Reset',
|
||||
'ETag',
|
||||
'Last-Modified',
|
||||
'X-GitHub-Request-Id',
|
||||
'X-NF-Request-ID',
|
||||
];
|
||||
|
||||
const transformRecordedData = (expectation, toSanitize) => {
|
||||
const { httpRequest, httpResponse } = expectation;
|
||||
|
||||
const responseHeaders = {};
|
||||
|
||||
Object.keys(httpResponse.headers).forEach(key => {
|
||||
responseHeaders[key] = httpResponse.headers[key][0];
|
||||
});
|
||||
Object.keys(httpResponse.headers)
|
||||
.filter(key => !HEADERS_TO_IGNORE.includes(key))
|
||||
.forEach(key => {
|
||||
responseHeaders[key] = httpResponse.headers[key][0];
|
||||
});
|
||||
|
||||
let responseBody = null;
|
||||
if (httpResponse.body && httpResponse.body.string) {
|
||||
responseBody = httpResponse.body.string;
|
||||
} else if (
|
||||
httpResponse.body &&
|
||||
httpResponse.body.type === 'BINARY' &&
|
||||
httpResponse.body.base64Bytes
|
||||
) {
|
||||
responseBody = {
|
||||
encoding: 'base64',
|
||||
content: httpResponse.body.base64Bytes,
|
||||
};
|
||||
} else if (httpResponse.body) {
|
||||
console.log('Unsupported response body:', JSON.stringify(httpResponse.body));
|
||||
}
|
||||
|
||||
// replace recorded user with fake one
|
||||
@ -345,13 +374,19 @@ const transformRecordedData = (expectation, toSanitize) => {
|
||||
|
||||
let body;
|
||||
if (httpRequest.body && httpRequest.body.string) {
|
||||
const bodyObject = JSON.parse(httpRequest.body.string);
|
||||
if (bodyObject.encoding === 'base64') {
|
||||
// sanitize encoded data
|
||||
const decodedBody = Buffer.from(bodyObject.content, 'base64').toString();
|
||||
bodyObject.content = Buffer.from(sanitizeString(decodedBody, toSanitize)).toString('base64');
|
||||
body = JSON.stringify(bodyObject);
|
||||
} else {
|
||||
try {
|
||||
const bodyObject = JSON.parse(httpRequest.body.string);
|
||||
if (bodyObject.encoding === 'base64') {
|
||||
// sanitize encoded data
|
||||
const decodedBody = Buffer.from(bodyObject.content, 'base64').toString('binary');
|
||||
const sanitizedContent = sanitizeString(decodedBody, toSanitize);
|
||||
const sanitizedEncodedContent = Buffer.from(sanitizedContent, 'binary').toString('base64');
|
||||
bodyObject.content = sanitizedEncodedContent;
|
||||
body = JSON.stringify(bodyObject);
|
||||
} else {
|
||||
body = httpRequest.body.string;
|
||||
}
|
||||
} catch (e) {
|
||||
body = httpRequest.body.string;
|
||||
}
|
||||
}
|
||||
@ -368,7 +403,11 @@ const transformRecordedData = (expectation, toSanitize) => {
|
||||
return cypressRouteOptions;
|
||||
};
|
||||
|
||||
async function teardownGitHubTest(taskData) {
|
||||
const defaultOptions = {
|
||||
transformRecordedData,
|
||||
};
|
||||
|
||||
async function teardownGitHubTest(taskData, { transformRecordedData } = defaultOptions) {
|
||||
if (process.env.RECORD_FIXTURES) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
@ -409,6 +448,8 @@ async function teardownGitHubTest(taskData) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transformRecordedData,
|
||||
getGitClient,
|
||||
setupGitHub,
|
||||
teardownGitHub,
|
||||
setupGitHubTest,
|
||||
|
@ -11,10 +11,18 @@
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
require('dotenv').config();
|
||||
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
|
||||
|
||||
const { setupGitHub, teardownGitHub, setupGitHubTest, teardownGitHubTest } = require('./github');
|
||||
const {
|
||||
setupGitGateway,
|
||||
teardownGitGateway,
|
||||
setupGitGatewayTest,
|
||||
teardownGitGatewayTest,
|
||||
} = require('./gitGateway');
|
||||
const { copyBackendFiles } = require('../utils/config');
|
||||
|
||||
module.exports = async on => {
|
||||
module.exports = async (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
on('task', {
|
||||
async setupBackend({ backend, options }) {
|
||||
@ -22,8 +30,13 @@ module.exports = async on => {
|
||||
await copyBackendFiles(backend);
|
||||
|
||||
let result = null;
|
||||
if (backend === 'github') {
|
||||
result = await setupGitHub(options);
|
||||
switch (backend) {
|
||||
case 'github':
|
||||
result = await setupGitHub(options);
|
||||
break;
|
||||
case 'git-gateway':
|
||||
result = await setupGitGateway(options);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -32,8 +45,13 @@ module.exports = async on => {
|
||||
const { backend } = taskData;
|
||||
console.log('Tearing down backend', backend);
|
||||
|
||||
if (backend === 'github') {
|
||||
await teardownGitHub(taskData);
|
||||
switch (backend) {
|
||||
case 'github':
|
||||
await teardownGitHub(taskData);
|
||||
break;
|
||||
case 'git-gateway':
|
||||
await teardownGitGateway(taskData);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('Restoring defaults');
|
||||
@ -45,8 +63,13 @@ module.exports = async on => {
|
||||
const { backend, testName } = taskData;
|
||||
console.log(`Setting up single test '${testName}' for backend`, backend);
|
||||
|
||||
if (backend === 'github') {
|
||||
await setupGitHubTest(taskData);
|
||||
switch (backend) {
|
||||
case 'github':
|
||||
await setupGitHubTest(taskData);
|
||||
break;
|
||||
case 'git-gateway':
|
||||
await setupGitGatewayTest(taskData);
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -56,26 +79,42 @@ module.exports = async on => {
|
||||
|
||||
console.log(`Tearing down single test '${testName}' for backend`, backend);
|
||||
|
||||
if (backend === 'github') {
|
||||
await teardownGitHubTest(taskData);
|
||||
switch (backend) {
|
||||
case 'github':
|
||||
await teardownGitHubTest(taskData);
|
||||
break;
|
||||
case 'git-gateway':
|
||||
await teardownGitGatewayTest(taskData);
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// to allows usage of a mock proxy
|
||||
on('before:browser:launch', (browser = {}, args) => {
|
||||
if (browser.name === 'chrome') {
|
||||
// to allows usage of a mock proxy
|
||||
args.push('--ignore-certificate-errors');
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
if (browser.name === 'electron') {
|
||||
// to allows usage of a mock proxy
|
||||
args['ignore-certificate-errors'] = true;
|
||||
// https://github.com/cypress-io/cypress/issues/2102
|
||||
if (browser.isHeaded) {
|
||||
args['width'] = 1200;
|
||||
args['height'] = 1200;
|
||||
} else {
|
||||
args['width'] = 1200;
|
||||
args['height'] = process.platform === 'darwin' ? 1178 : 1200;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
});
|
||||
|
||||
addMatchImageSnapshotPlugin(on, config);
|
||||
};
|
||||
|
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 59 KiB |