feat: v4.0.0 (#1016)
Co-authored-by: Denys Konovalov <kontakt@denyskon.de> Co-authored-by: Mathieu COSYNS <64072917+Mathieu-COSYNS@users.noreply.github.com>
72
cypress/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Cypress Tests Guide
|
||||
|
||||
## Introduction
|
||||
|
||||
[Cypress](https://www.cypress.io/) is a JavaScript End to End Testing Framework that runs in the browser.
|
||||
|
||||
Cypress tests run with a [local version](../packages/core/dev-test) of the CMS.
|
||||
|
||||
During the setup of a spec file, the relevant `index.html` and `config.yml` are copied from `packages/core/dev-test/backends/<backend>` to `dev-test`.
|
||||
|
||||
Tests for the `test` backend use mock data generated in `dev-test/backends/test/index.html`.
|
||||
|
||||
Tests for the other backends use previously [recorded data](fixtures) and stub `fetch` [calls](support/commands.js#L52). See more about recording tests data [here](#recording-tests-data).
|
||||
|
||||
## Run Tests Locally
|
||||
|
||||
```bash
|
||||
yarn test:e2e # builds the demo site and runs Cypress in headless mode with mock data
|
||||
```
|
||||
|
||||
## Debug Tests
|
||||
|
||||
```bash
|
||||
yarn develop # starts a local dev server with the demo site
|
||||
yarn test:e2e:exec # runs Cypress in non-headless mode with mock data
|
||||
```
|
||||
|
||||
## Recording Tests Data
|
||||
|
||||
When recording tests, access to the relevant backend API is required, thus one must set up a `.env` file in the root project directory in the following format:
|
||||
|
||||
```bash
|
||||
GITHUB_REPO_OWNER=owner
|
||||
GITHUB_REPO_NAME=repo
|
||||
GITHUB_REPO_TOKEN=tokenWithWritePermissions
|
||||
GITHUB_OPEN_AUTHORING_OWNER=forkOwner
|
||||
GITHUB_OPEN_AUTHORING_TOKEN=tokenWithWritePermissions
|
||||
|
||||
GITLAB_REPO_OWNER=owner
|
||||
GITLAB_REPO_NAME=repo
|
||||
GITLAB_REPO_TOKEN=tokenWithWritePermissions
|
||||
|
||||
BITBUCKET_REPO_OWNER=owner
|
||||
BITBUCKET_REPO_NAME=repo
|
||||
BITBUCKET_OUATH_CONSUMER_KEY=ouathConsumerKey
|
||||
BITBUCKET_OUATH_CONSUMER_SECRET=ouathConsumerSecret
|
||||
|
||||
NETLIFY_API_TOKEN=netlifyApiToken
|
||||
NETLIFY_INSTALLATION_ID=netlifyGitHubInstallationId
|
||||
```
|
||||
|
||||
> The structure of the relevant repo should match the settings in [`config.yml`](../packages/core/dev-test/backends/<backend>/config.yml#L1)
|
||||
|
||||
To start a recording run the following commands:
|
||||
|
||||
```bash
|
||||
yarn develop # starts a local dev server with the demo site
|
||||
yarn mock:server:start # starts the recording proxy
|
||||
yarn test:e2e:record-fixtures:dev # runs Cypress in non-headless and pass data through the recording proxy
|
||||
yarn mock:server:stop # stops the recording proxy
|
||||
```
|
||||
|
||||
> During the recorded process a clone of the relevant repo will be created under `.temp` and reset between tests.
|
||||
|
||||
Recordings are sanitized from any possible sensitive data and [transformed](plugins/common.js#L34) into an easier to process format.
|
||||
|
||||
To avoid recording all the tests over and over again, a recommended process is to:
|
||||
|
||||
1. Mark the specific test as `only` by changing `it("some test...` to `it.only("some test...` for the relevant test.
|
||||
2. Run the test in recording mode.
|
||||
3. Exit Cypress and stop the proxy.
|
||||
4. Run the test normally (with mock data) to verify the recording works.
|
82
cypress/cypress.d.ts
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type {
|
||||
SetupBackendProps,
|
||||
SetupBackendTestProps,
|
||||
SeedRepoProps,
|
||||
TeardownBackendTestProps,
|
||||
TeardownBackendProps,
|
||||
} from './interface';
|
||||
import type { Config as CMSConfig, DeepPartial } from '@staticcms/core/interface';
|
||||
|
||||
interface KeyProps {
|
||||
shift?: boolean;
|
||||
times?: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
task(event: 'setupBackend', props: SetupBackendProps): Chainable<SetupBackendResponse>;
|
||||
task(event: 'setupBackendTest', props: SetupBackendTestProps): Chainable<Promise<null>>;
|
||||
task(event: 'seedRepo', props: SeedRepoProps): Chainable<Promise<null>>;
|
||||
task(event: 'teardownBackendTest', props: TeardownBackendTestProps): Chainable<Promise<null>>;
|
||||
task(event: 'teardownBackend', props: TeardownBackendProps): Chainable<Promise<null>>;
|
||||
task(event: 'updateConfig', props: DeepPartial<CMSConfig>): Chainable<Promise<null>>;
|
||||
|
||||
login(): Chainable;
|
||||
loginAndNewPost(): Chainable;
|
||||
|
||||
dragTo(selector: string, options?: { delay?: number }): Chainable;
|
||||
|
||||
getMarkdownEditor(): Chainable;
|
||||
confirmMarkdownEditorContent(expected: string): Chainable;
|
||||
clearMarkdownEditorContent(): Chainable;
|
||||
confirmRawEditorContent(expected: string): Chainable;
|
||||
|
||||
enter(props?: KeyProps): Chainable;
|
||||
backspace(props?: KeyProps): Chainable;
|
||||
selectAll(props?: KeyProps): Chainable;
|
||||
up(props?: KeyProps): Chainable;
|
||||
down(props?: KeyProps): Chainable;
|
||||
left(props?: KeyProps): Chainable;
|
||||
right(props?: KeyProps): Chainable;
|
||||
tabkey(props?: KeyProps): Chainable;
|
||||
|
||||
selection(
|
||||
fn: (this: Cypress.ObjectLike, currentSubject: JQuery<any>) => Chainable,
|
||||
): Chainable;
|
||||
setSelection(
|
||||
query:
|
||||
| string
|
||||
| {
|
||||
anchorQuery: string;
|
||||
anchorOffset?: number;
|
||||
focusQuery: string;
|
||||
focusOffset?: number;
|
||||
},
|
||||
endQuery: string,
|
||||
): Chainable;
|
||||
|
||||
setCursor(query: string, atStart?: boolean): Chainable;
|
||||
setCursorBefore(query: string): Chainable;
|
||||
setCursorAfter(query: string): Chainable;
|
||||
|
||||
print(message: string): Chainable;
|
||||
|
||||
insertCodeBlock(): Chainable;
|
||||
insertEditorComponent(title: string): Chainable;
|
||||
|
||||
clickToolbarButton(title: string, opts: { times: number }): Chainable;
|
||||
clickHeadingOneButton(opts: { times: number }): Chainable;
|
||||
clickHeadingTwoButton(opts: { times: number }): Chainable;
|
||||
clickOrderedListButton(opts: { times: number }): Chainable;
|
||||
clickUnorderedListButton(opts: { times: number }): Chainable;
|
||||
clickCodeButton(opts: { times: number }): Chainable;
|
||||
clickItalicButton(opts: { times: number }): Chainable;
|
||||
clickQuoteButton(opts: { times: number }): Chainable;
|
||||
clickLinkButton(opts: { times: number }): Chainable;
|
||||
clickModeToggle(): Chainable;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'bitbucket';
|
||||
|
||||
describe('BitBucket Backend Editorial Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow' }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
const provider = 'github';
|
||||
|
||||
describe('Git Gateway (GitHub) Backend Editorial Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
const provider = 'gitlab';
|
||||
|
||||
describe('Git Gateway (GitLab) Backend Editorial Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('Github Backend Editorial Workflow - GraphQL API', () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: true, open_authoring: false },
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import fixture from '../common/open_authoring';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('Github Backend Editorial Workflow - GraphQL API - Open Authoring', () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: true, open_authoring: true },
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
getForkUser: () => taskResult.data.forkUser,
|
||||
});
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('Github Backend Editorial Workflow - REST API', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: false, open_authoring: false },
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import fixture from '../common/open_authoring';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('Github Backend Editorial Workflow - REST API - Open Authoring', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: false, open_authoring: true },
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
getForkUser: () => taskResult.data.forkUser,
|
||||
});
|
||||
});
|
36
cypress/e2e/_old/editorial_workflow_spec_gitlab_backend.js
Normal file
@ -0,0 +1,36 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'gitlab';
|
||||
|
||||
describe('GitLab Backend Editorial Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow' }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (
|
||||
Cypress.mocha.getRunner().suite.ctx.currentTest.title ===
|
||||
'can change status on and publish multiple entries'
|
||||
) {
|
||||
Cypress.mocha.getRunner().suite.ctx.currentTest.skip();
|
||||
}
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
import fixture from '../common/editorial_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'proxy';
|
||||
const mode = 'git';
|
||||
|
||||
describe.skip(`Proxy Backend Editorial Workflow - '${mode}' mode`, () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', mode }, backend);
|
||||
Cypress.config('defaultCommandTimeout', 5 * 1000);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
54
cypress/e2e/_old/field_validations_spec.js
Normal file
@ -0,0 +1,54 @@
|
||||
import {
|
||||
login,
|
||||
validateObjectFieldsAndExit,
|
||||
validateNestedObjectFieldsAndExit,
|
||||
validateListFieldsAndExit,
|
||||
validateNestedListFieldsAndExit,
|
||||
} from '../../utils/steps';
|
||||
import { setting1, setting2 } from '../../utils/constants';
|
||||
|
||||
describe('Test Backend Editorial Workflow', () => {
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
it('can validate object fields', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.contains('a', 'Posts').click();
|
||||
|
||||
validateObjectFieldsAndExit(setting1);
|
||||
});
|
||||
|
||||
it('can validate fields nested in an object field', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.contains('a', 'Posts').click();
|
||||
|
||||
validateNestedObjectFieldsAndExit(setting1);
|
||||
});
|
||||
|
||||
it('can validate list fields', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.contains('a', 'Posts').click();
|
||||
|
||||
validateListFieldsAndExit(setting2);
|
||||
});
|
||||
|
||||
it('can validate deeply nested list fields', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.contains('a', 'Posts').click();
|
||||
|
||||
validateNestedListFieldsAndExit(setting2);
|
||||
});
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import fixture from '../common/i18n_editorial_workflow_spec';
|
||||
|
||||
const backend = 'test';
|
||||
|
||||
describe(`I18N Test Backend Editorial Workflow`, () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', {
|
||||
backend,
|
||||
options: {
|
||||
publish_mode: 'editorial_workflow',
|
||||
i18n: {
|
||||
locales: ['en', 'de', 'fr'],
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
folder: 'content/i18n',
|
||||
i18n: true,
|
||||
fields: [{ i18n: true }, {}, { i18n: 'duplicate' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend });
|
||||
});
|
||||
|
||||
const entry = {
|
||||
Title: 'first title',
|
||||
Body: 'first body',
|
||||
};
|
||||
|
||||
fixture({ entry, getUser: () => taskResult.data.user });
|
||||
});
|
148
cypress/e2e/_old/i18n_simple_workflow_spec_proxy_fs_backend.js
Normal file
@ -0,0 +1,148 @@
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { login } from '../../utils/steps';
|
||||
import { createEntryTranslateAndPublish } from '../common/i18n';
|
||||
|
||||
const backend = 'proxy';
|
||||
const mode = 'fs';
|
||||
|
||||
const expectedEnContent = `---
|
||||
template: post
|
||||
title: first title
|
||||
date: 1970-01-01T00:00:00.000Z
|
||||
description: first description
|
||||
category: first category
|
||||
tags:
|
||||
- tag1
|
||||
---
|
||||
`;
|
||||
|
||||
const expectedDeContent = `---
|
||||
title: de
|
||||
date: 1970-01-01T00:00:00.000Z
|
||||
---
|
||||
`;
|
||||
|
||||
const expectedFrContent = `---
|
||||
title: fr
|
||||
date: 1970-01-01T00:00:00.000Z
|
||||
---
|
||||
`;
|
||||
|
||||
const contentSingleFile = `---
|
||||
en:
|
||||
template: post
|
||||
date: 1970-01-01T00:00:00.000Z
|
||||
title: first title
|
||||
body: first body
|
||||
description: first description
|
||||
category: first category
|
||||
tags:
|
||||
- tag1
|
||||
de:
|
||||
date: 1970-01-01T00:00:00.000Z
|
||||
title: de
|
||||
fr:
|
||||
date: 1970-01-01T00:00:00.000Z
|
||||
title: fr
|
||||
---
|
||||
`;
|
||||
|
||||
describe(`I18N Proxy Backend Simple Workflow - '${mode}' mode`, () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
const entry = {
|
||||
Title: 'first title',
|
||||
Body: 'first body',
|
||||
Description: 'first description',
|
||||
Category: 'first category',
|
||||
Tags: 'tag1',
|
||||
};
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
mode,
|
||||
publish_mode: 'simple',
|
||||
i18n: {
|
||||
locales: ['en', 'de', 'fr'],
|
||||
},
|
||||
collections: [{ i18n: true, fields: [{}, { i18n: true }, {}, { i18n: 'duplicate' }] }],
|
||||
},
|
||||
backend,
|
||||
);
|
||||
Cypress.config('taskTimeout', 15 * 1000);
|
||||
Cypress.config('defaultCommandTimeout', 5 * 1000);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
it('can create entry with translation in locale_folders mode', () => {
|
||||
cy.task('updateConfig', { i18n: { structure: 'multiple_folders' } });
|
||||
|
||||
login({ user: taskResult.data.user });
|
||||
|
||||
createEntryTranslateAndPublish(entry);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/en/1970-01-01-first-title.md`).should(
|
||||
'contain',
|
||||
expectedEnContent,
|
||||
);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/de/1970-01-01-first-title.md`).should(
|
||||
'eq',
|
||||
expectedDeContent,
|
||||
);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/fr/1970-01-01-first-title.md`).should(
|
||||
'eq',
|
||||
expectedFrContent,
|
||||
);
|
||||
});
|
||||
|
||||
it('can create entry with translation in single_file mode', () => {
|
||||
cy.task('updateConfig', { i18n: { structure: 'multiple_files' } });
|
||||
|
||||
login({ user: taskResult.data.user });
|
||||
|
||||
createEntryTranslateAndPublish(entry);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.en.md`).should(
|
||||
'contain',
|
||||
expectedEnContent,
|
||||
);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.de.md`).should(
|
||||
'eq',
|
||||
expectedDeContent,
|
||||
);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.fr.md`).should(
|
||||
'eq',
|
||||
expectedFrContent,
|
||||
);
|
||||
});
|
||||
|
||||
it('can create entry with translation in locale_file_extensions mode', () => {
|
||||
cy.task('updateConfig', { i18n: { structure: 'single_file' } });
|
||||
|
||||
login({ user: taskResult.data.user });
|
||||
|
||||
createEntryTranslateAndPublish(entry);
|
||||
|
||||
cy.readFile(`${taskResult.data.tempDir}/content/posts/1970-01-01-first-title.md`).should(
|
||||
'eq',
|
||||
contentSingleFile,
|
||||
);
|
||||
});
|
||||
});
|
76
cypress/e2e/_old/markdown_widget_backspace_spec.js
Normal file
@ -0,0 +1,76 @@
|
||||
describe('Markdown widget', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
// describe('pressing backspace', () => {
|
||||
it('sets non-default block to default when empty', () => {
|
||||
cy.focused().clickHeadingOneButton().backspace().confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('moves to previous block when no character left to delete', () => {
|
||||
cy.focused().type('foo').enter().clickHeadingOneButton().type('a').backspace({ times: 2 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
`);
|
||||
});
|
||||
it('does nothing at start of first block in document when non-empty and non-default', () => {
|
||||
cy.focused().clickHeadingOneButton().type('foo').setCursorBefore('foo').backspace({ times: 4 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>foo</h1>
|
||||
`);
|
||||
});
|
||||
it('deletes individual characters in middle of non-empty non-default block in document', () => {
|
||||
cy.focused().clickHeadingOneButton().type('foo').setCursorAfter('fo').backspace({ times: 3 })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>o</h1>
|
||||
`);
|
||||
});
|
||||
it('at beginning of non-first block, moves default block content to previous block', () => {
|
||||
cy
|
||||
.focused()
|
||||
.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setCursorBefore('bar')
|
||||
.backspace().confirmMarkdownEditorContent(`
|
||||
<h1>foobar</h1>
|
||||
`);
|
||||
});
|
||||
it('at beginning of non-first block, moves non-default block content to previous block', () => {
|
||||
cy
|
||||
.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.clickHeadingOneButton()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.clickHeadingTwoButton()
|
||||
.type('baz')
|
||||
.setCursorBefore('baz')
|
||||
.backspace()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>foo</p>
|
||||
<h1>barbaz</h1>
|
||||
`,
|
||||
)
|
||||
.setCursorBefore('bar')
|
||||
.backspace().confirmMarkdownEditorContent(`
|
||||
<p>foobarbaz</p>
|
||||
`);
|
||||
// });
|
||||
});
|
||||
});
|
134
cypress/e2e/_old/markdown_widget_code_block_spec.js
Normal file
@ -0,0 +1,134 @@
|
||||
import { oneLineTrim, stripIndent } from 'common-tags';
|
||||
|
||||
describe('Markdown widget code block', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
describe('code block', () => {
|
||||
it('outputs code', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.insertCodeBlock()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
${codeBlock(`
|
||||
foo
|
||||
bar
|
||||
`)}
|
||||
`,
|
||||
)
|
||||
.wait(500)
|
||||
.clickModeToggle().confirmMarkdownEditorContent(`
|
||||
${codeBlockRaw(`
|
||||
foo
|
||||
bar
|
||||
`)}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function codeBlockRaw(content) {
|
||||
return ['```', ...stripIndent(content).split('\n'), '```']
|
||||
.map(
|
||||
line => oneLineTrim`
|
||||
<div>
|
||||
<span>
|
||||
<span>
|
||||
<span>${line}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function codeBlock(content) {
|
||||
const lines = stripIndent(content)
|
||||
.split('\n')
|
||||
.map(
|
||||
(line, idx) => `
|
||||
<div>
|
||||
<div>
|
||||
<div>${idx + 1}</div>
|
||||
</div>
|
||||
<pre><span>${line}</span></pre>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
return oneLineTrim`
|
||||
<div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div><label>Code Block </label>
|
||||
<div><button><span><svg>
|
||||
<path></path>
|
||||
</svg></span></button>
|
||||
<div>
|
||||
<div>
|
||||
<div><textarea></textarea></div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<pre><span>xxxxxxxxxx</span></pre>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div> </div>
|
||||
</div>
|
||||
<div>
|
||||
${lines}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
<span>
|
||||
<span></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
80
cypress/e2e/_old/markdown_widget_enter_spec.js
Normal file
@ -0,0 +1,80 @@
|
||||
describe('Markdown widget breaks', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('pressing enter', () => {
|
||||
it('creates new default block from empty block', () => {
|
||||
cy.focused().enter().confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('creates new default block when selection collapsed at end of block', () => {
|
||||
cy.focused().type('foo').enter().confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('creates new default block when selection collapsed at end of non-default block', () => {
|
||||
cy.clickHeadingOneButton().type('foo').enter().confirmMarkdownEditorContent(`
|
||||
<h1>foo</h1>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('creates new default block when selection collapsed in empty non-default block', () => {
|
||||
cy.clickHeadingOneButton().enter().confirmMarkdownEditorContent(`
|
||||
<h1></h1>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('splits block into two same-type blocks when collapsed selection at block start', () => {
|
||||
cy.clickHeadingOneButton().type('foo').setCursorBefore('foo').enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1></h1>
|
||||
<h1>foo</h1>
|
||||
`);
|
||||
});
|
||||
it('splits block into two same-type blocks when collapsed in middle of selection at block start', () => {
|
||||
cy.clickHeadingOneButton().type('foo').setCursorBefore('oo').enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>f</h1>
|
||||
<h1>oo</h1>
|
||||
`);
|
||||
});
|
||||
it('deletes selected content and splits to same-type block when selection is expanded', () => {
|
||||
cy.clickHeadingOneButton().type('foo bar').setSelection('o b').enter()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<h1>fo</h1>
|
||||
<h1>ar</h1>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pressing shift+enter', () => {
|
||||
it('creates line break', () => {
|
||||
cy.focused().enter({ shift: true }).confirmMarkdownEditorContent(`
|
||||
<p>
|
||||
|
||||
</p>
|
||||
`);
|
||||
});
|
||||
it('creates consecutive line break', () => {
|
||||
cy.focused().enter({ shift: true, times: 4 }).confirmMarkdownEditorContent(`
|
||||
<p>
|
||||
|
||||
</p>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
109
cypress/e2e/_old/markdown_widget_hotkeys_spec.js
Normal file
@ -0,0 +1,109 @@
|
||||
import { HOT_KEY_MAP } from '../../utils/constants';
|
||||
const headingNumberToWord = ['', 'one', 'two', 'three', 'four', 'five', 'six'];
|
||||
const isMac = Cypress.platform === 'darwin';
|
||||
const modifierKey = isMac ? '{meta}' : '{ctrl}';
|
||||
// eslint-disable-next-line func-style
|
||||
const replaceMod = str => str.replace(/mod\+/g, modifierKey).replace(/shift\+/g, '{shift}');
|
||||
|
||||
describe('Markdown widget hotkeys', () => {
|
||||
describe('hot keys', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
cy.focused().type('foo').setSelection('foo').as('selection');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('bold', () => {
|
||||
it('pressing mod+b bolds the text', () => {
|
||||
cy.get('@selection')
|
||||
.type(replaceMod(HOT_KEY_MAP['bold']))
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>
|
||||
<strong>foo</strong>
|
||||
</p>
|
||||
`,
|
||||
)
|
||||
.type(replaceMod(HOT_KEY_MAP['bold']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('italic', () => {
|
||||
it('pressing mod+i italicizes the text', () => {
|
||||
cy.get('@selection')
|
||||
.type(replaceMod(HOT_KEY_MAP['italic']))
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>
|
||||
<em>foo</em>
|
||||
</p>
|
||||
`,
|
||||
)
|
||||
.type(replaceMod(HOT_KEY_MAP['italic']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('strikethrough', () => {
|
||||
it('pressing mod+shift+s displays a strike through the text', () => {
|
||||
cy.get('@selection')
|
||||
.type(replaceMod(HOT_KEY_MAP['strikethrough']))
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>
|
||||
<s>foo</s>
|
||||
</p>
|
||||
`,
|
||||
)
|
||||
.type(replaceMod(HOT_KEY_MAP['strikethrough']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('code', () => {
|
||||
it('pressing mod+shift+c displays a code block around the text', () => {
|
||||
cy.get('@selection')
|
||||
.type(replaceMod(HOT_KEY_MAP['code']))
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>
|
||||
<code>foo</code>
|
||||
</p>
|
||||
`,
|
||||
)
|
||||
.type(replaceMod(HOT_KEY_MAP['code']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('link', () => {
|
||||
before(() => {});
|
||||
it('pressing mod+k transforms the text to a link', () => {
|
||||
cy.window().then(win => {
|
||||
cy.get('@selection').type(replaceMod(HOT_KEY_MAP['link']));
|
||||
cy.stub(win, 'prompt').returns('https://google.com');
|
||||
cy.confirmMarkdownEditorContent('<p><a>foo</a></p>').type(
|
||||
replaceMod(HOT_KEY_MAP['link']),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('headings', () => {
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
it(`pressing mod+${i} transforms the text to a heading`, () => {
|
||||
cy.get('@selection')
|
||||
.type(replaceMod(HOT_KEY_MAP[`heading-${headingNumberToWord[i]}`]))
|
||||
.confirmMarkdownEditorContent(`<h${i}>foo</h${i}>`)
|
||||
.type(replaceMod(HOT_KEY_MAP[`heading-${headingNumberToWord[i]}`]));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
64
cypress/e2e/_old/markdown_widget_link_spec.js
Normal file
@ -0,0 +1,64 @@
|
||||
describe('Markdown widget link', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('link', () => {
|
||||
it('can add a new valid link', () => {
|
||||
const link = 'https://www.staticcms.org/';
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'prompt').returns(link);
|
||||
});
|
||||
cy.focused().clickLinkButton();
|
||||
|
||||
cy.confirmMarkdownEditorContent(`<p><a>${link}</a></p>`);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(300);
|
||||
cy.clickModeToggle();
|
||||
|
||||
cy.confirmRawEditorContent(`<${link}>`);
|
||||
});
|
||||
|
||||
it('can add a new invalid link', () => {
|
||||
const link = 'www.staticcms.org';
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'prompt').returns(link);
|
||||
});
|
||||
cy.focused().clickLinkButton();
|
||||
|
||||
cy.confirmMarkdownEditorContent(`<p><a>${link}</a></p>`);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(300);
|
||||
cy.clickModeToggle();
|
||||
|
||||
cy.confirmRawEditorContent(`[${link}](${link})`);
|
||||
});
|
||||
|
||||
it('can select existing text as link', () => {
|
||||
const link = 'https://www.staticcms.org';
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'prompt').returns(link);
|
||||
});
|
||||
|
||||
const text = 'Static CMS';
|
||||
cy.focused().getMarkdownEditor().type(text).setSelection(text).clickLinkButton();
|
||||
|
||||
cy.confirmMarkdownEditorContent(`<p><a>${text}</a></p>`);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(300);
|
||||
cy.clickModeToggle();
|
||||
|
||||
cy.confirmRawEditorContent(`[${text}](${link})`);
|
||||
});
|
||||
});
|
||||
});
|
734
cypress/e2e/_old/markdown_widget_list_spec.js
Normal file
@ -0,0 +1,734 @@
|
||||
describe('Markdown widget', () => {
|
||||
describe('list', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
// describe('toolbar buttons', () => {
|
||||
it('creates and focuses empty list', () => {
|
||||
cy.clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('removes list', () => {
|
||||
cy.clickUnorderedListButton().clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('converts a list item to a paragraph block which is a sibling of the parent list', () => {
|
||||
cy.clickUnorderedListButton().type('foo').enter().clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('converts empty nested list item to empty paragraph block in parent list item', () => {
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.tabkey()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.backspace({ times: 4 })
|
||||
.clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('moves nested list item content to parent list item when in first block', () => {
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.tabkey()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.tabkey()
|
||||
.type('baz')
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.up()
|
||||
.clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('affects only the current block with collapsed selection', () => {
|
||||
cy
|
||||
.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.type('baz')
|
||||
.up()
|
||||
.clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>baz</p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('wrap each bottom-most block in a selection with a list item block', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.type('baz')
|
||||
.setSelection('foo', 'baz')
|
||||
.wait(500)
|
||||
.clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('unwraps list item block from each selected list item and unwraps all of them from the outer list block', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.type('baz')
|
||||
.setSelection('foo', 'baz')
|
||||
.wait(500)
|
||||
.clickUnorderedListButton().confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
<p>baz</p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('combines adjacent same-typed lists, not differently typed lists', () => {
|
||||
cy.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.type('baz')
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.up()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>baz</p>
|
||||
`,
|
||||
)
|
||||
.down({ times: 2 })
|
||||
.focused()
|
||||
.clickUnorderedListButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.up()
|
||||
.enter()
|
||||
.type('qux')
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.up()
|
||||
.enter()
|
||||
.type('quux')
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>quux</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.clickOrderedListButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>quux</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<p>qux</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.setSelection({
|
||||
anchorQuery: 'ul > li > ol p',
|
||||
anchorOffset: 1,
|
||||
focusQuery: 'ul > li > ul:last-child p',
|
||||
focusOffset: 2,
|
||||
});
|
||||
});
|
||||
|
||||
// while this works on dev environemnt, it will always fail in cypress - has something to do with text selection
|
||||
// it('affects only selected list items', () => {
|
||||
// cy
|
||||
// .clickUnorderedListButton()
|
||||
// .type('foo')
|
||||
// .enter()
|
||||
// .type('bar')
|
||||
// .enter()
|
||||
// .type('baz')
|
||||
// .setSelection('bar')
|
||||
// .clickUnorderedListButton()
|
||||
// .confirmMarkdownEditorContent(
|
||||
// `
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// <p>bar</p>
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>baz</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// `,
|
||||
// )
|
||||
// .clickUnorderedListButton()
|
||||
// .setSelection('bar', 'baz')
|
||||
// .clickUnorderedListButton()
|
||||
// .confirmMarkdownEditorContent(
|
||||
// `
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// <p>bar</p>
|
||||
// <p>baz</p>
|
||||
// `,
|
||||
// )
|
||||
// .clickUnorderedListButton()
|
||||
// .confirmMarkdownEditorContent(
|
||||
// `
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// <li>
|
||||
// <p>bar</p>
|
||||
// </li>
|
||||
// <li>
|
||||
// <p>baz</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// `,
|
||||
// )
|
||||
// .setSelection('baz')
|
||||
// .clickUnorderedListButton()
|
||||
// .confirmMarkdownEditorContent(
|
||||
// `
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// <li>
|
||||
// <p>bar</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// <p>baz</p>
|
||||
// `,
|
||||
// )
|
||||
// .clickUnorderedListButton()
|
||||
// .tabkey()
|
||||
// .setCursorAfter('baz')
|
||||
// .enter()
|
||||
// .tabkey()
|
||||
// .type('qux')
|
||||
// .confirmMarkdownEditorContent(
|
||||
// `
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// <li>
|
||||
// <p>bar</p>
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>baz</p>
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>qux</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </li>
|
||||
// </ul>
|
||||
// `,
|
||||
// )
|
||||
// .setSelection('baz')
|
||||
// .clickOrderedListButton()
|
||||
// .confirmMarkdownEditorContent(
|
||||
// `
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// <li>
|
||||
// <p>bar</p>
|
||||
// <ol>
|
||||
// <li>
|
||||
// <p>baz</p>
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>qux</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </li>
|
||||
// </ol>
|
||||
// </li>
|
||||
// </ul>
|
||||
// `,
|
||||
// )
|
||||
// .setCursorAfter('qux')
|
||||
// .enter({ times: 2 })
|
||||
// .clickUnorderedListButton()
|
||||
// .confirmMarkdownEditorContent(`
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>foo</p>
|
||||
// </li>
|
||||
// <li>
|
||||
// <p>bar</p>
|
||||
// <ol>
|
||||
// <li>
|
||||
// <p>baz</p>
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p>qux</p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </li>
|
||||
// </ol>
|
||||
// <ul>
|
||||
// <li>
|
||||
// <p></p>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </li>
|
||||
// </ul>
|
||||
// `);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('on Enter', () => {
|
||||
it('removes the list item and list if empty', () => {
|
||||
cy.clickUnorderedListButton().enter().confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a new list item in a non-empty list', () => {
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.type('bar')
|
||||
.enter().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a new default block below a list when hitting Enter twice on an empty list item of the list', () => {
|
||||
cy.clickUnorderedListButton().type('foo').enter({ times: 2 }).confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
// });
|
||||
|
||||
// describe('on Backspace', () => {
|
||||
it('removes the list item and list if empty', () => {
|
||||
cy.clickUnorderedListButton().backspace().confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
|
||||
it('removes the list item if list not empty', () => {
|
||||
cy.clickUnorderedListButton().type('foo').enter().backspace().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not remove list item if empty with non-default block', () => {
|
||||
cy.clickUnorderedListButton().clickHeadingOneButton().backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
// });
|
||||
|
||||
// describe('on Tab', () => {
|
||||
it('does nothing in top level list', () => {
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.type('foo')
|
||||
.tabkey().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('indents nested list items', () => {
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.tabkey()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.enter()
|
||||
.tabkey().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('only nests up to one level down from the parent list', () => {
|
||||
cy.clickUnorderedListButton().type('foo').enter().tabkey().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('unindents nested list items with shift', () => {
|
||||
cy.clickUnorderedListButton().type('foo').enter().tabkey().tabkey({ shift: true })
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p></p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
|
||||
it('indents and unindents from one level below parent back to document root', () => {
|
||||
cy
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.tabkey()
|
||||
.type('bar')
|
||||
.enter()
|
||||
.tabkey()
|
||||
.type('baz')
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.tabkey({ shift: true })
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.tabkey({ shift: true }).confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
});
|
||||
// });
|
||||
});
|
||||
});
|
31
cypress/e2e/_old/markdown_widget_marks_spec.js
Normal file
@ -0,0 +1,31 @@
|
||||
describe('Markdown widget', () => {
|
||||
describe('code mark', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
describe('toolbar button', () => {
|
||||
it('can combine code mark with other marks', () => {
|
||||
cy.clickItalicButton().type('foo').setSelection('oo').clickCodeButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>
|
||||
<em>f</em>
|
||||
<code>
|
||||
<em>oo</em>
|
||||
</code>
|
||||
</p>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
370
cypress/e2e/_old/markdown_widget_quote_spec.js
Normal file
@ -0,0 +1,370 @@
|
||||
describe('Markdown widget', () => {
|
||||
describe('quote block', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAndNewPost();
|
||||
cy.clearMarkdownEditorContent();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
// describe('toggle quote', () => {
|
||||
it('toggles empty quote block on and off in empty editor', () => {
|
||||
cy
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<p></p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton().confirmMarkdownEditorContent(`
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('toggles empty quote block on and off for current block', () => {
|
||||
cy
|
||||
.focused()
|
||||
.type('foo')
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton().confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
`);
|
||||
});
|
||||
it('toggles entire quote block without expanded selection', () => {
|
||||
cy.clickQuoteButton().type('foo').enter().type('bar').clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
`);
|
||||
});
|
||||
it('toggles entire quote block with complex content', () => {
|
||||
cy
|
||||
.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickHeadingOneButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 }) // First Enter creates new list item. Second Enter turns that list item into a default block.
|
||||
.clickQuoteButton() // Unwrap the quote block.
|
||||
.confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<h1>foo</h1>
|
||||
</li>
|
||||
</ul>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
it('toggles empty quote block on and off for selected blocks', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setSelection('foo', 'bar')
|
||||
.wait(500)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton().confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('toggles empty quote block on and off for partially selected blocks', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.focused()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setSelection('oo', 'ba')
|
||||
.wait(500)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton().confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('toggles quote block on and off for multiple selected list items', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.focused()
|
||||
.clickUnorderedListButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setSelection('foo', 'bar')
|
||||
.wait(500)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.clickQuoteButton()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
</ul>
|
||||
`,
|
||||
)
|
||||
.setCursorAfter('bar')
|
||||
.wait(500)
|
||||
.enter()
|
||||
.type('baz')
|
||||
.setSelection('bar', 'baz')
|
||||
.wait(500)
|
||||
.clickQuoteButton().confirmMarkdownEditorContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<p>bar</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>baz</p>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('creates new quote block if parent is not a quote, can deeply nest', () => {
|
||||
cy.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickQuoteButton()
|
||||
.clickUnorderedListButton()
|
||||
.clickQuoteButton()
|
||||
.type('foo')
|
||||
// Content should contains 4 <blockquote> tags and 3 <ul> tags
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
/*
|
||||
* First Enter creates new paragraph within the innermost block quote.
|
||||
* Second Enter moves that paragraph one level up to become sibling of the previous quote block and direct child of a list item.
|
||||
* Third Enter to turn that paragraph into a list item and move it one level up.
|
||||
* Repeat the circle for three more times to reach the second list item of the outermost list block.
|
||||
* Then Enter again to turn that list item into a paragraph and move it one level up to become sibling of the outermost list and
|
||||
* direct child of the outermost block quote.
|
||||
*/
|
||||
.enter({ times: 10 })
|
||||
.type('bar')
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
/* The GOAL is to delete all the text content inside this deeply nested block quote and turn it into a default paragraph block on top level.
|
||||
* We need:
|
||||
* 3 Backspace to delete the word “bar”.
|
||||
* 1 Backspace to remove the paragraph that contains bar and bring cursor to the end of the unordered list which is direct child of the outermost block quote.
|
||||
* 3 Backspace to remove the word “foo”.
|
||||
* 1 Backspace to remove the current block quote that the cursor is on, 1 Backspace to remove the list that wraps the block quote. Repeat this step for three times for a total of 6 Backspace until the cursor is on the outermost block quote.
|
||||
* 1 Backspace to remove to toggle off the outermost block quote and turn it into a default paragraph.
|
||||
* Total Backspaces required: 3 + 1 + 3 + ((1 + 1) * 3) + 1 = 14
|
||||
*/
|
||||
.backspace({ times: 14 });
|
||||
});
|
||||
// });
|
||||
|
||||
// describe('backspace inside quote', () => {
|
||||
it('joins two paragraphs', () => {
|
||||
cy.clickQuoteButton().type('foo').enter().type('bar').setCursorBefore('bar').backspace()
|
||||
.confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foobar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('joins quote with previous quote', () => {
|
||||
cy
|
||||
.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter({ times: 2 })
|
||||
.clickQuoteButton()
|
||||
.type('bar')
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
<blockquote>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.setCursorBefore('bar')
|
||||
.backspace().confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('removes first block from quote when focused at first block at start', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.type('bar')
|
||||
.setCursorBefore('foo')
|
||||
.wait(500)
|
||||
.backspace().confirmMarkdownEditorContent(`
|
||||
<p>foo</p>
|
||||
<blockquote>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
// });
|
||||
|
||||
// describe('enter inside quote', () => {
|
||||
it('creates new block inside quote', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy
|
||||
.clickQuoteButton()
|
||||
.type('foo')
|
||||
.enter()
|
||||
.confirmMarkdownEditorContent(
|
||||
`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p></p>
|
||||
</blockquote>
|
||||
`,
|
||||
)
|
||||
.type('bar')
|
||||
.setCursorAfter('ba')
|
||||
.wait(500)
|
||||
.enter().confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
<p>ba</p>
|
||||
<p>r</p>
|
||||
</blockquote>
|
||||
`);
|
||||
});
|
||||
it('creates new block after quote from empty last block', () => {
|
||||
cy.clickQuoteButton().type('foo').enter().enter().confirmMarkdownEditorContent(`
|
||||
<blockquote>
|
||||
<p>foo</p>
|
||||
</blockquote>
|
||||
<p></p>
|
||||
`);
|
||||
});
|
||||
// });
|
||||
});
|
||||
});
|
27
cypress/e2e/_old/media_library_spec_bitbucket_backend.js
Normal file
@ -0,0 +1,27 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'bitbucket';
|
||||
|
||||
describe('BitBucket Backend Media Library - REST API', () => {
|
||||
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,28 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'bitbucket';
|
||||
const lfs = true;
|
||||
|
||||
describe('BitBucket Backend Media Library - Large Media', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { lfs }, 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,28 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
const provider = 'github';
|
||||
|
||||
describe('Git Gateway (GitHub) Backend Media Library - Large Media', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, 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,28 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
const provider = 'gitlab';
|
||||
|
||||
describe('Git Gateway (GitLab) Backend Media Library - Large Media', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', provider }, 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,34 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('GitHub Backend Media Library - GraphQL API', () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: true },
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
34
cypress/e2e/_old/media_library_spec_github_backend_rest.js
Normal file
@ -0,0 +1,34 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('GitHub Backend Media Library - REST API', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: false },
|
||||
publish_mode: 'editorial_workflow',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
27
cypress/e2e/_old/media_library_spec_gitlab_backend.js
Normal file
@ -0,0 +1,27 @@
|
||||
import fixture from '../common/media_library';
|
||||
import { entry1 } from '../common/entries';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
const backend = 'gitlab';
|
||||
|
||||
describe('GitLab Backend Media Library - REST API', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow' }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
29
cypress/e2e/_old/media_library_spec_proxy_git_backend.js
Normal file
@ -0,0 +1,29 @@
|
||||
import fixture from '../common/media_library';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1 } from '../common/entries';
|
||||
|
||||
const backend = 'proxy';
|
||||
const mode = 'git';
|
||||
|
||||
describe(`Proxy Backend Media Library - '${mode}' mode`, () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'editorial_workflow', mode }, backend);
|
||||
Cypress.config('defaultCommandTimeout', 5 * 1000);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({ entries: [entry1], getUser: () => taskResult.data.user });
|
||||
});
|
21
cypress/e2e/_old/media_library_spec_test_backend.js
Normal file
@ -0,0 +1,21 @@
|
||||
import fixture from '../common/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 });
|
||||
});
|
71
cypress/e2e/_old/search_suggestion_spec.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { login } from '../../utils/steps';
|
||||
|
||||
const search = (term, collection) => {
|
||||
cy.get('[class*=SearchInput]').clear({ force: true });
|
||||
cy.get('[class*=SearchInput]').type(term, { force: true });
|
||||
cy.get('[class*=SuggestionsContainer]').within(() => {
|
||||
cy.contains(collection).click();
|
||||
});
|
||||
};
|
||||
|
||||
const assertSearchHeading = title => {
|
||||
cy.get('[class*=SearchResultHeading]').should('have.text', title);
|
||||
};
|
||||
|
||||
const assertSearchResult = (text, collection) => {
|
||||
cy.get('[class*=ListCardLink] h2').contains(collection ?? text);
|
||||
};
|
||||
|
||||
const assertNotInSearch = text => {
|
||||
cy.get('[class*=ListCardLink] h2').contains(text).should('not.exist');
|
||||
};
|
||||
|
||||
describe('Search Suggestion', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
it('can search in all collections', () => {
|
||||
search('this', 'All Collections');
|
||||
|
||||
assertSearchHeading('Search Results for "this"');
|
||||
|
||||
assertSearchResult('This is post # 20', 'Posts');
|
||||
assertSearchResult('This is a TOML front matter post', 'Posts');
|
||||
assertSearchResult('This is a JSON front matter post', 'Posts');
|
||||
assertSearchResult('This is a YAML front matter post', 'Posts');
|
||||
assertSearchResult('This FAQ item # 5', 'FAQ');
|
||||
});
|
||||
|
||||
it('can search in posts collection', () => {
|
||||
search('this', 'Posts');
|
||||
|
||||
assertSearchHeading('Search Results for "this" in Posts');
|
||||
|
||||
assertSearchResult('This is post # 20');
|
||||
assertSearchResult('This is a TOML front matter post');
|
||||
assertSearchResult('This is a JSON front matter post');
|
||||
assertSearchResult('This is a YAML front matter post');
|
||||
|
||||
assertNotInSearch('This FAQ item # 5');
|
||||
});
|
||||
|
||||
it('can search in faq collection', () => {
|
||||
search('this', 'FAQ');
|
||||
|
||||
assertSearchHeading('Search Results for "this" in FAQ');
|
||||
|
||||
assertSearchResult('This FAQ item # 5');
|
||||
|
||||
assertNotInSearch('This is post # 20');
|
||||
});
|
||||
});
|
30
cypress/e2e/_old/simple_workflow_spec_bitbucket_backend.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
|
||||
import type { TaskResult } from '../../interface';
|
||||
|
||||
const backend = 'bitbucket';
|
||||
|
||||
describe('BitBucket Backend Simple Workflow', () => {
|
||||
let taskResult: TaskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple' }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
getUser: () => taskResult.data?.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
const provider = 'github';
|
||||
|
||||
describe('Git Gateway (GitHub) Backend Simple Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple', provider }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'git-gateway';
|
||||
const provider = 'gitlab';
|
||||
|
||||
describe('Git Gateway (GitLab) Backend Simple Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple', provider }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('GitHub Backend Simple Workflow - GraphQL API', () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: true },
|
||||
publish_mode: 'simple',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
37
cypress/e2e/_old/simple_workflow_spec_github_backend_rest.js
Normal file
@ -0,0 +1,37 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'github';
|
||||
|
||||
describe('GitHub Backend Simple Workflow - REST API', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(
|
||||
taskResult,
|
||||
{
|
||||
backend: { use_graphql: false },
|
||||
publish_mode: 'simple',
|
||||
},
|
||||
backend,
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
30
cypress/e2e/_old/simple_workflow_spec_gitlab_backend.js
Normal file
@ -0,0 +1,30 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'gitlab';
|
||||
|
||||
describe('GitLab Backend Simple Workflow', () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple' }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
32
cypress/e2e/_old/simple_workflow_spec_proxy_fs_backend.js
Normal file
@ -0,0 +1,32 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'proxy';
|
||||
const mode = 'fs';
|
||||
|
||||
describe(`Proxy Backend Simple Workflow - '${mode}' mode`, () => {
|
||||
const taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple', mode }, backend);
|
||||
Cypress.config('defaultCommandTimeout', 5 * 1000);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
35
cypress/e2e/_old/simple_workflow_spec_proxy_git_backend.js
Normal file
@ -0,0 +1,35 @@
|
||||
import fixture from '../common/simple_workflow';
|
||||
import * as specUtils from '../common/spec_utils';
|
||||
import { entry1, entry2, entry3 } from '../common/entries';
|
||||
|
||||
const backend = 'proxy';
|
||||
const mode = 'git';
|
||||
|
||||
describe(`Proxy Backend Simple Workflow - '${mode}' mode`, () => {
|
||||
let taskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple', mode }, backend);
|
||||
Cypress.config('defaultCommandTimeout', 5 * 1000);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(taskResult, backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (Cypress.mocha.getRunner().suite.ctx.currentTest.title === 'can create an entry') {
|
||||
Cypress.mocha.getRunner().suite.ctx.currentTest.skip();
|
||||
}
|
||||
specUtils.beforeEach(taskResult, backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(taskResult, backend);
|
||||
});
|
||||
|
||||
fixture({
|
||||
entries: [entry1, entry2, entry3],
|
||||
getUser: () => taskResult.data.user,
|
||||
});
|
||||
});
|
92
cypress/e2e/common/editorial_workflow.js
Normal file
@ -0,0 +1,92 @@
|
||||
import {
|
||||
login,
|
||||
createPost,
|
||||
createPostAndExit,
|
||||
updateExistingPostAndExit,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
goToCollections,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
assertWorkflowStatusInEditor,
|
||||
assertPublishedEntry,
|
||||
deleteEntryInEditor,
|
||||
assertOnCollectionsPage,
|
||||
assertEntryDeleted,
|
||||
assertWorkflowStatus,
|
||||
updateWorkflowStatusInEditor,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus, editorStatus } from '../../utils/constants';
|
||||
|
||||
export default function ({ entries, getUser }) {
|
||||
it('successfully loads', () => {
|
||||
login({ user: getUser() });
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
});
|
||||
|
||||
it('can update an entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
updateExistingPostAndExit(entries[0], entries[1]);
|
||||
});
|
||||
|
||||
it('can publish an editorial workflow entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entries[0]);
|
||||
});
|
||||
|
||||
it('can change workflow status', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
|
||||
updateWorkflowStatus(entries[0], workflowStatus.review, workflowStatus.ready);
|
||||
updateWorkflowStatus(entries[0], workflowStatus.ready, workflowStatus.review);
|
||||
updateWorkflowStatus(entries[0], workflowStatus.review, workflowStatus.draft);
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
|
||||
});
|
||||
|
||||
it('can change status on and publish multiple entries', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
createPostAndExit(entries[1]);
|
||||
createPostAndExit(entries[2]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[2], workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entries[1], workflowStatus.draft, workflowStatus.ready);
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entries[2]);
|
||||
publishWorkflowEntry(entries[1]);
|
||||
publishWorkflowEntry(entries[0]);
|
||||
goToCollections();
|
||||
assertPublishedEntry([entries[2], entries[1], entries[0]]);
|
||||
});
|
||||
|
||||
it('can delete an entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPost(entries[0]);
|
||||
deleteEntryInEditor();
|
||||
assertOnCollectionsPage();
|
||||
assertEntryDeleted(entries[0]);
|
||||
});
|
||||
|
||||
it('can update workflow status from within the editor', () => {
|
||||
login({ user: getUser() });
|
||||
createPost(entries[0]);
|
||||
assertWorkflowStatusInEditor(editorStatus.draft);
|
||||
updateWorkflowStatusInEditor(editorStatus.review);
|
||||
assertWorkflowStatusInEditor(editorStatus.review);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
assertWorkflowStatusInEditor(editorStatus.ready);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entries[0], workflowStatus.ready);
|
||||
});
|
||||
}
|
50
cypress/e2e/common/editorial_workflow_migrations.js
Normal file
@ -0,0 +1,50 @@
|
||||
import {
|
||||
login,
|
||||
createPostAndExit,
|
||||
goToWorkflow,
|
||||
goToCollections,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
assertPublishedEntry,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus } from '../../utils/constants';
|
||||
|
||||
const versions = ['2.9.7', '2.10.24'];
|
||||
|
||||
export default function ({ entries, getUser }) {
|
||||
versions.forEach(version => {
|
||||
it(`migrate from ${version} to latest`, () => {
|
||||
cy.task('switchToVersion', {
|
||||
version,
|
||||
});
|
||||
cy.reload();
|
||||
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
createPostAndExit(entries[1]);
|
||||
createPostAndExit(entries[2]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[2], workflowStatus.draft, workflowStatus.ready);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1500); // older versions of the CMS didn't wait fully for the update to be resolved
|
||||
updateWorkflowStatus(entries[1], workflowStatus.draft, workflowStatus.ready);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1500); // older versions of the CMS didn't wait fully for the update to be resolved
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1500); // older versions of the CMS didn't wait fully for the update to be resolved
|
||||
|
||||
cy.task('switchToVersion', {
|
||||
version: 'latest',
|
||||
});
|
||||
cy.reload();
|
||||
|
||||
// allow migration code to run for 5 minutes
|
||||
publishWorkflowEntry(entries[2], 5 * 60 * 1000);
|
||||
publishWorkflowEntry(entries[1]);
|
||||
publishWorkflowEntry(entries[0]);
|
||||
goToCollections();
|
||||
assertPublishedEntry([entries[2], entries[1], entries[0]]);
|
||||
});
|
||||
});
|
||||
}
|
121
cypress/e2e/common/entries.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import type { Post } from '@staticcms/cypress/interface';
|
||||
|
||||
export const entry1: Post = {
|
||||
Title: 'first title',
|
||||
Body: 'first body',
|
||||
Description: 'first description',
|
||||
Category: 'first category',
|
||||
Tags: 'tag1',
|
||||
};
|
||||
|
||||
export const entry2: Post = {
|
||||
Title: 'second title',
|
||||
Body: 'second body',
|
||||
Description: 'second description',
|
||||
Category: 'second category',
|
||||
Tags: 'tag2',
|
||||
};
|
||||
|
||||
export const entry3: Post = {
|
||||
Title: 'third title',
|
||||
Body: 'third body',
|
||||
Description: 'third description',
|
||||
Category: 'third category',
|
||||
Tags: 'tag3',
|
||||
};
|
||||
|
||||
export const entry4: Post = {
|
||||
Title: 'fourth title',
|
||||
Body: 'fourth body',
|
||||
Description: 'fourth description',
|
||||
Category: 'fourth category',
|
||||
Tags: 'tag4',
|
||||
};
|
||||
|
||||
export const entry5: Post = {
|
||||
Title: 'fifth title',
|
||||
Body: 'fifth body',
|
||||
Description: 'fifth description',
|
||||
Category: 'fifth category',
|
||||
Tags: 'tag5',
|
||||
};
|
||||
|
||||
export const entry6: Post = {
|
||||
Title: 'sixth title',
|
||||
Body: 'sixth body',
|
||||
Description: 'sixth description',
|
||||
Category: 'sixth category',
|
||||
Tags: 'tag6',
|
||||
};
|
||||
|
||||
export const entry7: Post = {
|
||||
Title: 'seventh title',
|
||||
Body: 'seventh body',
|
||||
Description: 'seventh description',
|
||||
Category: 'seventh category',
|
||||
Tags: 'tag7',
|
||||
};
|
||||
|
||||
export const entry8: Post = {
|
||||
Title: 'eighth title',
|
||||
Body: 'eighth body',
|
||||
Description: 'eighth description',
|
||||
Category: 'eighth category',
|
||||
Tags: 'tag8',
|
||||
};
|
||||
|
||||
export const entry9: Post = {
|
||||
Title: 'nineth title',
|
||||
Body: 'nineth body',
|
||||
Description: 'nineth description',
|
||||
Category: 'nineth category',
|
||||
Tags: 'tag9',
|
||||
};
|
||||
|
||||
export const entry10: Post = {
|
||||
Title: 'tenth title',
|
||||
Body: 'tenth body',
|
||||
Description: 'tenth description',
|
||||
Category: 'tenth category',
|
||||
Tags: 'tag10',
|
||||
};
|
||||
|
||||
export const entry11: Post = {
|
||||
Title: 'eleventh title',
|
||||
Body: 'eleventh body',
|
||||
Description: 'eleventh description',
|
||||
Category: 'eleventh category',
|
||||
Tags: 'tag11',
|
||||
};
|
||||
|
||||
export const entry12: Post = {
|
||||
Title: 'twelveth title',
|
||||
Body: 'twelveth body',
|
||||
Description: 'twelveth description',
|
||||
Category: 'twelveth category',
|
||||
Tags: 'tag12',
|
||||
};
|
||||
|
||||
export const entry13: Post = {
|
||||
Title: 'thirteenth title',
|
||||
Body: 'thirteenth body',
|
||||
Description: 'thirteenth description',
|
||||
Category: 'thirteenth category',
|
||||
Tags: 'tag13',
|
||||
};
|
||||
|
||||
export const entry14: Post = {
|
||||
Title: 'fourteenth title',
|
||||
Body: 'fourteenth body',
|
||||
Description: 'fourteenth description',
|
||||
Category: 'fourteenth category',
|
||||
Tags: 'tag14',
|
||||
};
|
||||
|
||||
export const entry15: Post = {
|
||||
Title: 'fifteenth title',
|
||||
Body: 'fifteenth body',
|
||||
Description: 'fifteenth description',
|
||||
Category: 'fifteenth category',
|
||||
Tags: 'tag15',
|
||||
};
|
55
cypress/e2e/common/i18n.js
Normal file
@ -0,0 +1,55 @@
|
||||
import { newPost, populateEntry, publishEntry, flushClockAndSave } from '../../utils/steps';
|
||||
|
||||
const enterTranslation = str => {
|
||||
cy.get('[data-testid="field-Title"]').first().clear({ force: true });
|
||||
cy.get('[data-testid="field-Title"]').first().type(str, { force: true });
|
||||
};
|
||||
|
||||
const createAndTranslate = entry => {
|
||||
newPost();
|
||||
// fill the main entry
|
||||
populateEntry(entry, () => undefined);
|
||||
|
||||
// fill the translation
|
||||
cy.get('.Pane2').within(() => {
|
||||
enterTranslation('de');
|
||||
|
||||
cy.contains('span', 'Writing in DE').click();
|
||||
cy.contains('span', 'fr').click();
|
||||
|
||||
enterTranslation('fr');
|
||||
});
|
||||
};
|
||||
|
||||
export const updateTranslation = () => {
|
||||
cy.get('.Pane2').within(() => {
|
||||
enterTranslation('fr fr');
|
||||
|
||||
cy.contains('span', 'Writing in FR').click();
|
||||
cy.contains('span', 'de').click();
|
||||
|
||||
enterTranslation('de de');
|
||||
});
|
||||
cy.contains('button', 'Save').click();
|
||||
};
|
||||
|
||||
export const assertTranslation = () => {
|
||||
cy.get('.Pane2').within(() => {
|
||||
cy.get('[data-testid="field-Title"]').should('have.value', 'de');
|
||||
|
||||
cy.contains('span', 'Writing in DE').click();
|
||||
cy.contains('span', 'fr').click();
|
||||
|
||||
cy.get('[data-testid="field-Title"]').should('have.value', 'fr');
|
||||
});
|
||||
};
|
||||
|
||||
export const createEntryTranslateAndPublish = entry => {
|
||||
createAndTranslate(entry);
|
||||
publishEntry();
|
||||
};
|
||||
|
||||
export const createEntryTranslateAndSave = entry => {
|
||||
createAndTranslate(entry);
|
||||
flushClockAndSave();
|
||||
};
|
53
cypress/e2e/common/i18n_editorial_workflow_spec.js
Normal file
@ -0,0 +1,53 @@
|
||||
import {
|
||||
login,
|
||||
goToWorkflow,
|
||||
updateWorkflowStatus,
|
||||
exitEditor,
|
||||
publishWorkflowEntry,
|
||||
goToEntry,
|
||||
updateWorkflowStatusInEditor,
|
||||
publishEntryInEditor,
|
||||
assertPublishedEntryInEditor,
|
||||
assertUnpublishedEntryInEditor,
|
||||
assertUnpublishedChangesInEditor,
|
||||
} from '../../utils/steps';
|
||||
import { createEntryTranslateAndSave, assertTranslation, updateTranslation } from './i18n';
|
||||
import { workflowStatus, editorStatus, publishTypes } from '../../utils/constants';
|
||||
|
||||
export default function ({ entry, getUser }) {
|
||||
const structures = ['multiple_folders', 'multiple_files', 'single_file'];
|
||||
structures.forEach(structure => {
|
||||
it(`can create and publish entry with translation in ${structure} mode`, () => {
|
||||
cy.task('updateConfig', { i18n: { structure } });
|
||||
|
||||
login({ user: getUser() });
|
||||
|
||||
createEntryTranslateAndSave(entry);
|
||||
assertUnpublishedEntryInEditor();
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry);
|
||||
goToEntry(entry);
|
||||
assertTranslation();
|
||||
assertPublishedEntryInEditor();
|
||||
});
|
||||
|
||||
it(`can update translated entry in ${structure} mode`, () => {
|
||||
cy.task('updateConfig', { i18n: { structure: 'multiple_folders' } });
|
||||
|
||||
login({ user: getUser() });
|
||||
|
||||
createEntryTranslateAndSave(entry);
|
||||
assertUnpublishedEntryInEditor();
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishEntryInEditor(publishTypes.publishNow);
|
||||
exitEditor();
|
||||
goToEntry(entry);
|
||||
assertTranslation();
|
||||
assertPublishedEntryInEditor();
|
||||
updateTranslation();
|
||||
assertUnpublishedChangesInEditor();
|
||||
});
|
||||
});
|
||||
}
|
162
cypress/e2e/common/media_library.js
Normal file
@ -0,0 +1,162 @@
|
||||
import {
|
||||
login,
|
||||
goToMediaLibrary,
|
||||
newPost,
|
||||
populateEntry,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
goToEntry,
|
||||
goToCollections,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus } from '../../utils/constants';
|
||||
|
||||
function uploadMediaFile() {
|
||||
assertNoImagesInLibrary();
|
||||
|
||||
const fixture = 'cypress/fixtures/media/netlify.png';
|
||||
cy.get('input[type="file"]').selectFile(fixture, { force: true });
|
||||
cy.contains('span', 'Uploading...').should('not.exist');
|
||||
|
||||
assertImagesInLibrary();
|
||||
}
|
||||
|
||||
function assertImagesInLibrary() {
|
||||
cy.get('img[class*="CardImage"]').should('exist');
|
||||
}
|
||||
|
||||
function assertNoImagesInLibrary() {
|
||||
cy.get('h1').contains('Loading...').should('not.exist');
|
||||
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').should('not.be.disabled');
|
||||
cy.contains('button', 'Choose selected').click();
|
||||
}
|
||||
|
||||
function chooseAnImage() {
|
||||
cy.contains('button', 'Choose an image').click();
|
||||
}
|
||||
|
||||
function waitForEntryToLoad() {
|
||||
cy.contains('button', 'Saving...').should('not.exist');
|
||||
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);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(500);
|
||||
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]);
|
||||
goToMediaLibrary();
|
||||
assertImagesInLibrary();
|
||||
matchImageSnapshot();
|
||||
});
|
||||
|
||||
it('should show published entry image in grid view', () => {
|
||||
publishPostWithImage(entries[0]);
|
||||
goToCollections();
|
||||
switchToGridView();
|
||||
assertGridEntryImage(entries[0]);
|
||||
|
||||
matchImageSnapshot();
|
||||
});
|
||||
}
|
75
cypress/e2e/common/open_authoring.js
Normal file
@ -0,0 +1,75 @@
|
||||
import {
|
||||
login,
|
||||
createPostAndExit,
|
||||
updateExistingPostAndExit,
|
||||
goToWorkflow,
|
||||
deleteWorkflowEntry,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
} from '../../utils/steps';
|
||||
import { workflowStatus } from '../../utils/constants';
|
||||
|
||||
export default function ({ entries, getUser, getForkUser }) {
|
||||
it('successfully loads', () => {
|
||||
login({ user: getUser() });
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
});
|
||||
|
||||
it('can update an entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
updateExistingPostAndExit(entries[0], entries[1]);
|
||||
});
|
||||
|
||||
it('can publish an editorial workflow entry', () => {
|
||||
login({ user: getUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entries[0]);
|
||||
});
|
||||
|
||||
it('successfully forks repository and loads', () => {
|
||||
login({ user: getForkUser() });
|
||||
});
|
||||
|
||||
it('can create an entry on fork', () => {
|
||||
login({ user: getForkUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
});
|
||||
|
||||
it('can update a draft entry on fork', () => {
|
||||
login({ user: getForkUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
updateExistingPostAndExit(entries[0], entries[1]);
|
||||
});
|
||||
|
||||
it('can change entry status from fork', () => {
|
||||
login({ user: getForkUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
|
||||
});
|
||||
|
||||
it('can delete review entry from fork', () => {
|
||||
login({ user: getForkUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
|
||||
deleteWorkflowEntry(entries[0]);
|
||||
});
|
||||
|
||||
it('can return entry to draft and delete it', () => {
|
||||
login({ user: getForkUser() });
|
||||
createPostAndExit(entries[0]);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entries[0], workflowStatus.draft, workflowStatus.review);
|
||||
|
||||
updateWorkflowStatus(entries[0], workflowStatus.review, workflowStatus.draft);
|
||||
deleteWorkflowEntry(entries[0]);
|
||||
});
|
||||
}
|
85
cypress/e2e/common/simple_workflow.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {
|
||||
assertPublishedEntry,
|
||||
createPostAndPublish,
|
||||
createPostPublishAndCreateNew,
|
||||
createPostPublishAndDuplicate,
|
||||
duplicatePostAndPublish,
|
||||
editPostAndPublish,
|
||||
editPostPublishAndCreateNew,
|
||||
editPostPublishAndDuplicate,
|
||||
login,
|
||||
} from '../../utils/steps';
|
||||
import {
|
||||
entry1,
|
||||
entry10,
|
||||
entry2,
|
||||
entry3,
|
||||
entry4,
|
||||
entry5,
|
||||
entry6,
|
||||
entry7,
|
||||
entry8,
|
||||
entry9,
|
||||
} from './entries';
|
||||
|
||||
import type { User } from '@staticcms/cypress/interface';
|
||||
|
||||
export interface SimpleWorkflowProps {
|
||||
getUser?: () => User | undefined;
|
||||
}
|
||||
|
||||
export default function ({ getUser }: SimpleWorkflowProps = {}) {
|
||||
it('successfully loads', () => {
|
||||
login({ user: getUser?.() });
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login({ user: getUser?.() });
|
||||
createPostAndPublish(entry1);
|
||||
assertPublishedEntry(entry1);
|
||||
});
|
||||
|
||||
it('can publish a new entry and create new', () => {
|
||||
login();
|
||||
createPostPublishAndCreateNew(entry2);
|
||||
assertPublishedEntry(entry2);
|
||||
});
|
||||
|
||||
it('can publish a new entry and duplicate', () => {
|
||||
login();
|
||||
createPostPublishAndDuplicate(entry3);
|
||||
assertPublishedEntry(entry3);
|
||||
});
|
||||
|
||||
it('can edit an existing entry and publish', () => {
|
||||
login();
|
||||
createPostAndPublish(entry4);
|
||||
assertPublishedEntry(entry4);
|
||||
|
||||
editPostAndPublish(entry4, entry5);
|
||||
});
|
||||
|
||||
it('can edit an existing entry, publish and create new', () => {
|
||||
login();
|
||||
createPostAndPublish(entry6);
|
||||
assertPublishedEntry(entry6);
|
||||
|
||||
editPostPublishAndCreateNew(entry6, entry7);
|
||||
});
|
||||
|
||||
it('can edit an existing entry, publish and duplicate', () => {
|
||||
login();
|
||||
createPostAndPublish(entry8);
|
||||
assertPublishedEntry(entry8);
|
||||
|
||||
editPostPublishAndDuplicate(entry8, entry9);
|
||||
});
|
||||
|
||||
it('can duplicate an existing entry', () => {
|
||||
login();
|
||||
createPostAndPublish(entry10);
|
||||
assertPublishedEntry(entry10);
|
||||
|
||||
duplicatePostAndPublish(entry10);
|
||||
});
|
||||
}
|
47
cypress/e2e/common/spec_utils.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { Config } from '@staticcms/core/interface';
|
||||
import type { TaskResult } from 'cypress/interface';
|
||||
|
||||
export const before = (taskResult: TaskResult, options: Partial<Config>, backend: string) => {
|
||||
Cypress.config('taskTimeout', 7 * 60 * 1000);
|
||||
cy.task('setupBackend', { backend, options }).then(data => {
|
||||
taskResult.data = data;
|
||||
});
|
||||
};
|
||||
|
||||
export const after = (backend: string) => {
|
||||
cy.task('teardownBackend', {
|
||||
backend,
|
||||
});
|
||||
};
|
||||
|
||||
export const beforeEach = (backend: string) => {
|
||||
cy.task('setupBackendTest', {
|
||||
backend,
|
||||
testName: Cypress.currentTest.title,
|
||||
});
|
||||
};
|
||||
|
||||
export const afterEach = (backend: string) => {
|
||||
cy.task('teardownBackendTest', {
|
||||
backend,
|
||||
testName: Cypress.currentTest.title,
|
||||
});
|
||||
|
||||
const {
|
||||
suite: {
|
||||
ctx: {
|
||||
currentTest: { state, _retries: retries, _currentRetry: currentRetry },
|
||||
},
|
||||
},
|
||||
} = (Cypress as any).mocha.getRunner();
|
||||
|
||||
if (state === 'failed' && retries === currentRetry) {
|
||||
(Cypress as any).runner.stop();
|
||||
}
|
||||
};
|
||||
|
||||
export const seedRepo = (backend: string) => {
|
||||
cy.task('seedRepo', {
|
||||
backend,
|
||||
});
|
||||
};
|
358
cypress/e2e/editorial_workflow_test_backend.spec.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import format from 'date-fns/format';
|
||||
|
||||
import {
|
||||
login,
|
||||
createPost,
|
||||
createPostAndExit,
|
||||
exitEditor,
|
||||
goToWorkflow,
|
||||
updateWorkflowStatus,
|
||||
publishWorkflowEntry,
|
||||
assertWorkflowStatusInEditor,
|
||||
assertPublishedEntry,
|
||||
deleteEntryInEditor,
|
||||
assertOnCollectionsPage,
|
||||
assertEntryDeleted,
|
||||
assertWorkflowStatus,
|
||||
updateWorkflowStatusInEditor,
|
||||
unpublishEntry,
|
||||
publishEntryInEditor,
|
||||
duplicateEntry,
|
||||
goToEntry,
|
||||
populateEntry,
|
||||
publishAndCreateNewEntryInEditor,
|
||||
publishAndDuplicateEntryInEditor,
|
||||
assertNotification,
|
||||
assertFieldValidationError,
|
||||
} from '../utils/steps';
|
||||
import { editorStatus, publishTypes, notifications } from '../utils/constants';
|
||||
import {
|
||||
entry1,
|
||||
entry10,
|
||||
entry11,
|
||||
entry12,
|
||||
entry13,
|
||||
entry14,
|
||||
entry15,
|
||||
entry2,
|
||||
entry3,
|
||||
entry4,
|
||||
entry5,
|
||||
entry6,
|
||||
entry7,
|
||||
entry8,
|
||||
entry9,
|
||||
} from './common/entries';
|
||||
import { WorkflowStatus } from '@staticcms/core/constants/publishModes';
|
||||
|
||||
describe('Test Backend Editorial Workflow', () => {
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.task('updateConfig', {
|
||||
collections: [{ publish: true }],
|
||||
publish_mode: 'editorial_workflow',
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully loads', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
});
|
||||
|
||||
it('can create an entry', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry1);
|
||||
|
||||
// new entry should show 'Delete unpublished entry'
|
||||
cy.wait(1000);
|
||||
cy.get('[data-testid="editor-extra-menu"]', { timeout: 5000 }).should('be.enabled');
|
||||
cy.get('[data-testid="editor-extra-menu"]').click();
|
||||
cy.contains('[data-testid="delete-button"]', 'Delete unpublished entry');
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`http://localhost:8080/#/collections/posts/entries/${format(
|
||||
new Date(),
|
||||
'yyyy-MM-dd',
|
||||
)}-${entry1.Title.toLowerCase().replace(/\s/, '-')}`,
|
||||
);
|
||||
exitEditor();
|
||||
});
|
||||
|
||||
it.only('can publish an editorial workflow entry', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPostAndExit(entry2);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry2, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
publishWorkflowEntry(entry2);
|
||||
});
|
||||
|
||||
it('can update an entry', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPostAndExit(entry3);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry3, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
publishWorkflowEntry(entry3);
|
||||
|
||||
goToEntry(entry3);
|
||||
populateEntry(entry4);
|
||||
// existing entry should show 'Delete unpublished changes'
|
||||
cy.contains('button', 'Delete unpublished changes');
|
||||
// existing entry slug should remain the same after save'
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`http://localhost:8080/#/collections/posts/entries/${format(
|
||||
new Date(),
|
||||
'yyyy-MM-dd',
|
||||
)}-${entry3.Title.toLowerCase().replace(/\s/, '-')}`,
|
||||
);
|
||||
exitEditor();
|
||||
});
|
||||
|
||||
it('can change workflow status', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPostAndExit(entry5);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry5, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_REVIEW);
|
||||
updateWorkflowStatus(entry5, WorkflowStatus.PENDING_REVIEW, WorkflowStatus.PENDING_PUBLISH);
|
||||
updateWorkflowStatus(entry5, WorkflowStatus.PENDING_PUBLISH, WorkflowStatus.PENDING_REVIEW);
|
||||
updateWorkflowStatus(entry5, WorkflowStatus.PENDING_REVIEW, WorkflowStatus.DRAFT);
|
||||
updateWorkflowStatus(entry5, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
});
|
||||
|
||||
it('can change status on and publish multiple entries', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPostAndExit(entry6);
|
||||
createPostAndExit(entry7);
|
||||
createPostAndExit(entry8);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry8, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
updateWorkflowStatus(entry7, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
updateWorkflowStatus(entry6, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
publishWorkflowEntry(entry8);
|
||||
publishWorkflowEntry(entry7);
|
||||
publishWorkflowEntry(entry6);
|
||||
assertPublishedEntry([entry8, entry7, entry6]);
|
||||
});
|
||||
|
||||
it('can delete an entry', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry9);
|
||||
deleteEntryInEditor();
|
||||
assertOnCollectionsPage();
|
||||
assertEntryDeleted(entry9);
|
||||
});
|
||||
|
||||
it('can update workflow status from within the editor', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry10);
|
||||
assertWorkflowStatusInEditor(editorStatus.draft);
|
||||
updateWorkflowStatusInEditor(editorStatus.review);
|
||||
assertWorkflowStatusInEditor(editorStatus.review);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
assertWorkflowStatusInEditor(editorStatus.ready);
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry10, WorkflowStatus.PENDING_PUBLISH);
|
||||
});
|
||||
|
||||
it('can unpublish an existing entry', () => {
|
||||
// first publish an entry
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPostAndExit(entry11);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry11, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
publishWorkflowEntry(entry11);
|
||||
// then unpublish it
|
||||
unpublishEntry(entry11);
|
||||
});
|
||||
|
||||
it('can duplicate an existing entry', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry12);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishEntryInEditor(publishTypes.publishNow);
|
||||
duplicateEntry(entry12);
|
||||
});
|
||||
|
||||
it('cannot publish when "publish" is false', () => {
|
||||
cy.task('updateConfig', { collections: [{ publish: false }] });
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry13);
|
||||
cy.contains('span', 'Publish').should('not.exist');
|
||||
exitEditor();
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry13, WorkflowStatus.DRAFT, WorkflowStatus.PENDING_PUBLISH);
|
||||
cy.contains('button', 'Publish new entry').should('not.exist');
|
||||
});
|
||||
|
||||
it('can create a new entry, publish and create new', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry14);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
|
||||
publishAndCreateNewEntryInEditor();
|
||||
});
|
||||
|
||||
it('can create a new entry, publish and duplicate', () => {
|
||||
login({ editorialWorkflow: true });
|
||||
|
||||
cy.get('[data-testid="sidebar-collection-nav-Posts').click();
|
||||
|
||||
createPost(entry15);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishAndDuplicateEntryInEditor(entry15);
|
||||
});
|
||||
|
||||
const inSidebar = (func: (currentSubject: JQuery<HTMLElement>) => void) => {
|
||||
cy.get('[class*=SidebarNavList]').within(func);
|
||||
};
|
||||
|
||||
const inGrid = (func: (currentSubject: JQuery<HTMLElement>) => void) => {
|
||||
cy.get('[class*=CardsGrid]').within(func);
|
||||
};
|
||||
|
||||
it('can access nested collection items', () => {
|
||||
login();
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
inSidebar(() => cy.contains('a', 'Directory'));
|
||||
inGrid(() => cy.contains('a', 'Root Page'));
|
||||
inGrid(() => cy.contains('a', 'Directory'));
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Directory').click());
|
||||
|
||||
inGrid(() => cy.contains('a', 'Sub Directory'));
|
||||
inGrid(() => cy.contains('a', 'Another Sub Directory'));
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Sub Directory').click());
|
||||
inGrid(() => cy.contains('a', 'Nested Directory'));
|
||||
cy.url().should(
|
||||
'eq',
|
||||
'http://localhost:8080/#/collections/pages/filter/directory/sub-directory',
|
||||
);
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
|
||||
inGrid(() => cy.contains('a', 'Another Sub Directory').should('not.exist'));
|
||||
});
|
||||
|
||||
it('can navigate to nested entry', () => {
|
||||
login();
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
inSidebar(() => cy.contains('a', 'Directory').click());
|
||||
inGrid(() => cy.contains('a', 'Another Sub Directory').click());
|
||||
|
||||
cy.url().should(
|
||||
'eq',
|
||||
'http://localhost:8080/#/collections/pages/entries/directory/another-sub-directory/index',
|
||||
);
|
||||
});
|
||||
|
||||
it(`can create a new entry with custom path`, () => {
|
||||
login();
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
inSidebar(() => cy.contains('a', 'Directory').click());
|
||||
inSidebar(() => cy.contains('a', 'Sub Directory').click());
|
||||
cy.contains('a', 'New Page').click();
|
||||
|
||||
cy.get('[data-testid="field-Path"]').should('have.value', 'directory/sub-directory');
|
||||
cy.get('[data-testid="field-Path"]').type('/new-path');
|
||||
cy.get('[data-testid="field-Title"]').type('New Path Title');
|
||||
cy.wait(150);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishEntryInEditor(publishTypes.publishNow);
|
||||
exitEditor();
|
||||
|
||||
inGrid(() => cy.contains('a', 'New Path Title'));
|
||||
inSidebar(() => cy.contains('a', 'Directory').click());
|
||||
inSidebar(() => cy.contains('a', 'Directory').click());
|
||||
inGrid(() => cy.contains('a', 'New Path Title').should('not.exist'));
|
||||
});
|
||||
|
||||
it(`can't create an entry with an existing path`, () => {
|
||||
login();
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
inSidebar(() => cy.contains('a', 'Directory').click());
|
||||
inSidebar(() => cy.contains('a', 'Sub Directory').click());
|
||||
|
||||
cy.contains('a', 'New Page').click();
|
||||
cy.get('[data-testid="field-Title"]').type('New Path Title');
|
||||
cy.wait(150);
|
||||
cy.contains('button', 'Save').click();
|
||||
|
||||
assertFieldValidationError({
|
||||
message: `Path 'directory/sub-directory' already exists`,
|
||||
fieldLabel: 'Path',
|
||||
});
|
||||
});
|
||||
|
||||
it('can move an existing entry to a new path', () => {
|
||||
login();
|
||||
|
||||
inSidebar(() => cy.contains('a', 'Pages').click());
|
||||
inGrid(() => cy.contains('a', 'Directory').click());
|
||||
|
||||
cy.get('[data-testid="field-Path"]').should('have.value', 'directory');
|
||||
cy.get('[data-testid="field-Path"]').clear();
|
||||
cy.get('[data-testid="field-Path"]').type('new-directory');
|
||||
cy.get('[data-testid="field-Title"]').clear();
|
||||
cy.get('[data-testid="field-Title"]').type('New Directory');
|
||||
cy.wait(150);
|
||||
cy.contains('button', 'Save').click();
|
||||
assertNotification(notifications.saved);
|
||||
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||
publishEntryInEditor(publishTypes.publishNow);
|
||||
exitEditor();
|
||||
|
||||
inSidebar(() => cy.contains('a', 'New Directory').click());
|
||||
|
||||
inGrid(() => cy.contains('a', 'Sub Directory'));
|
||||
inGrid(() => cy.contains('a', 'Another Sub Directory'));
|
||||
});
|
||||
});
|
28
cypress/e2e/simple_workflow_test_backend.spec.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import fixture from './common/simple_workflow';
|
||||
import * as specUtils from './common/spec_utils';
|
||||
|
||||
import type { TaskResult } from '../interface';
|
||||
|
||||
const backend = 'test';
|
||||
|
||||
describe('Test Backend Simple Workflow', () => {
|
||||
const taskResult: TaskResult = { data: {} };
|
||||
|
||||
before(() => {
|
||||
specUtils.before(taskResult, { publish_mode: 'simple' }, backend);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
specUtils.after(backend);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
specUtils.beforeEach(backend);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
specUtils.afterEach(backend);
|
||||
});
|
||||
|
||||
fixture();
|
||||
});
|
60
cypress/e2e/view_filters.spec.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { login } from '../utils/steps';
|
||||
|
||||
const filter = (term: string) => {
|
||||
cy.get('[data-testid="filter-by"]').click();
|
||||
cy.get(`[data-testid="filter-by-option-${term}"]`).click();
|
||||
};
|
||||
|
||||
const assertEntriesCount = (count: number) => {
|
||||
cy.get('[class*=CMS_Entries_entry-listing-table-row]').should('have.length', count);
|
||||
};
|
||||
|
||||
describe('View Filter', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
it('can apply string filter', () => {
|
||||
// enable filter
|
||||
filter('Posts With Index');
|
||||
|
||||
assertEntriesCount(20);
|
||||
|
||||
// disable filter
|
||||
filter('Posts With Index');
|
||||
});
|
||||
|
||||
it('can apply boolean filter', () => {
|
||||
// enable filter
|
||||
filter('Drafts');
|
||||
|
||||
assertEntriesCount(10);
|
||||
|
||||
// disable filter
|
||||
filter('Drafts');
|
||||
|
||||
assertEntriesCount(23);
|
||||
});
|
||||
|
||||
it('can apply multiple filters', () => {
|
||||
// enable filter
|
||||
filter('Posts Without Index');
|
||||
|
||||
assertEntriesCount(3);
|
||||
|
||||
filter('Drafts');
|
||||
|
||||
assertEntriesCount(0);
|
||||
|
||||
cy.contains('div', 'No Entries');
|
||||
});
|
||||
});
|
58
cypress/e2e/view_groups.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { login } from '../utils/steps';
|
||||
|
||||
const group = (term: string) => {
|
||||
cy.get('[data-testid="group-by"]').click();
|
||||
cy.get(`[data-testid="group-by-option-${term}"]`).click();
|
||||
};
|
||||
|
||||
const assertGroupsCount = (count: number) => {
|
||||
cy.get('[class*=CMS_Entries_group-button]').should('have.length', count);
|
||||
};
|
||||
|
||||
const assertEachGroupCount = (name: string, count: number) => {
|
||||
cy.get(`[data-testid="group-by-${name}"]`).click();
|
||||
assertEntriesCount(count);
|
||||
};
|
||||
|
||||
const assertEntriesCount = (count: number) => {
|
||||
cy.get('[class*=CMS_Entries_entry-listing-table-row]').should('have.length', count);
|
||||
};
|
||||
|
||||
describe('View Group', () => {
|
||||
before(() => {
|
||||
Cypress.config('defaultCommandTimeout', 4000);
|
||||
cy.task('setupBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.task('teardownBackend', { backend: 'test' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
it('can apply string group', () => {
|
||||
// enable group
|
||||
group('Year');
|
||||
|
||||
assertGroupsCount(2);
|
||||
const year = new Date().getFullYear();
|
||||
assertEachGroupCount(`Year ${year}`, 20);
|
||||
assertEachGroupCount('Year 2015', 3);
|
||||
|
||||
//disable group
|
||||
group('Year');
|
||||
|
||||
assertEntriesCount(23);
|
||||
|
||||
//enable group
|
||||
group('Drafts');
|
||||
|
||||
assertGroupsCount(3);
|
||||
|
||||
assertEachGroupCount('Drafts', 10);
|
||||
assertEachGroupCount('Not Drafts', 10);
|
||||
assertEachGroupCount('Other', 3);
|
||||
});
|
||||
});
|
BIN
cypress/fixtures/media/decap.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
cypress/fixtures/media/netlify.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
55
cypress/interface.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Config } from '@staticcms/core/interface';
|
||||
|
||||
export interface TaskResult {
|
||||
data: SetupBackendResponse;
|
||||
}
|
||||
|
||||
export interface SetupBackendProps {
|
||||
backend: string;
|
||||
options: Partial<Config>;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
email: string;
|
||||
password: string;
|
||||
netlifySiteURL?: string;
|
||||
}
|
||||
|
||||
export type SetupBackendResponse = null | {
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export interface SetupBackendTestProps {
|
||||
backend: string;
|
||||
testName: string;
|
||||
}
|
||||
|
||||
export interface TeardownBackendTestProps {
|
||||
backend: string;
|
||||
testName: string;
|
||||
}
|
||||
|
||||
export interface SeedRepoProps {
|
||||
backend: string;
|
||||
}
|
||||
|
||||
export interface TeardownBackendProps {
|
||||
backend: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
Title: string;
|
||||
Body: string;
|
||||
Description: string;
|
||||
Category: string;
|
||||
Tags: string;
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Authors {
|
||||
authors: Author[];
|
||||
}
|
310
cypress/plugins/bitbucket.js
Normal file
@ -0,0 +1,310 @@
|
||||
const fs = require('fs-extra');
|
||||
const fetch = require('node-fetch');
|
||||
const path = require('path');
|
||||
const { updateConfig } = require('../utils/config');
|
||||
const { escapeRegExp } = require('../utils/regexp');
|
||||
const {
|
||||
getExpectationsFilename,
|
||||
transformRecordedData: transformData,
|
||||
getGitClient,
|
||||
} = require('./common');
|
||||
const { merge } = require('lodash');
|
||||
|
||||
const BITBUCKET_REPO_OWNER_SANITIZED_VALUE = 'owner';
|
||||
const BITBUCKET_REPO_NAME_SANITIZED_VALUE = 'repo';
|
||||
const BITBUCKET_REPO_TOKEN_SANITIZED_VALUE = 'fakeToken';
|
||||
|
||||
const FAKE_OWNER_USER = {
|
||||
name: 'owner',
|
||||
display_name: 'owner',
|
||||
links: {
|
||||
avatar: {
|
||||
href: 'https://avatars1.githubusercontent.com/u/7892489?v=4',
|
||||
},
|
||||
},
|
||||
nickname: 'owner',
|
||||
};
|
||||
|
||||
async function getEnvs() {
|
||||
const {
|
||||
BITBUCKET_REPO_OWNER: owner,
|
||||
BITBUCKET_REPO_NAME: repo,
|
||||
BITBUCKET_OUATH_CONSUMER_KEY: consumerKey,
|
||||
BITBUCKET_OUATH_CONSUMER_SECRET: consumerSecret,
|
||||
} = process.env;
|
||||
if (!owner || !repo || !consumerKey || !consumerSecret) {
|
||||
throw new Error(
|
||||
'Please set BITBUCKET_REPO_OWNER, BITBUCKET_REPO_NAME, BITBUCKET_OUATH_CONSUMER_KEY, BITBUCKET_OUATH_CONSUMER_SECRET environment variables',
|
||||
);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('grant_type', 'client_credentials');
|
||||
|
||||
const { access_token: token } = await fetch(
|
||||
`https://${consumerKey}:${consumerSecret}@bitbucket.org/site/oauth2/access_token`,
|
||||
{ method: 'POST', body: params },
|
||||
).then(r => r.json());
|
||||
|
||||
return { owner, repo, token };
|
||||
}
|
||||
|
||||
const API_URL = 'https://api.bitbucket.org/2.0/';
|
||||
|
||||
function get(token, path) {
|
||||
return fetch(`${API_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
function post(token, path, body) {
|
||||
return fetch(`${API_URL}${path}`, {
|
||||
method: 'POST',
|
||||
...(body ? { body } : {}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(body ? { 'Content-Type': 'application/json' } : {}),
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function del(token, path) {
|
||||
return fetch(`${API_URL}${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareTestBitBucketRepo({ lfs }) {
|
||||
const { owner, repo, token } = await getEnvs();
|
||||
|
||||
// postfix a random string to avoid collisions
|
||||
const postfix = Math.random().toString(32).slice(2);
|
||||
const testRepoName = `${repo}-${Date.now()}-${postfix}`;
|
||||
|
||||
console.info('Creating repository', testRepoName, token);
|
||||
const response = await post(
|
||||
token,
|
||||
`repositories/${owner}/${testRepoName}`,
|
||||
JSON.stringify({ scm: 'git' }),
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to create repository. ${response.statusText}`);
|
||||
}
|
||||
|
||||
const tempDir = path.join('.temp', testRepoName);
|
||||
await fs.remove(tempDir);
|
||||
let git = getGitClient();
|
||||
|
||||
const repoUrl = `git@bitbucket.org:${owner}/${repo}.git`;
|
||||
|
||||
console.info('Cloning repository', repoUrl);
|
||||
await git.clone(repoUrl, tempDir);
|
||||
git = getGitClient(tempDir);
|
||||
|
||||
console.info('Pushing to new repository', testRepoName);
|
||||
|
||||
console.info('Updating remote...');
|
||||
await git.removeRemote('origin');
|
||||
await git.addRemote('origin', `git@bitbucket.org:${owner}/${testRepoName}`);
|
||||
|
||||
console.info('Pushing...');
|
||||
await git.push(['-u', 'origin', 'main']);
|
||||
|
||||
console.info('Pushed to new repository', testRepoName);
|
||||
|
||||
if (lfs) {
|
||||
console.info(`Enabling LFS for repo ${owner}/${repo}`);
|
||||
await git.addConfig('commit.gpgsign', 'false');
|
||||
await git.raw(['lfs', 'track', '*.png', '*.jpg']);
|
||||
await git.add('.gitattributes');
|
||||
await git.commit('chore: track images files under LFS');
|
||||
await git.push('origin', 'main');
|
||||
}
|
||||
|
||||
return { owner, repo: testRepoName, tempDir };
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
const { token } = await getEnvs();
|
||||
const user = await get(token, 'user');
|
||||
return { ...user, token, backendName: 'bitbucket' };
|
||||
}
|
||||
|
||||
async function deleteRepositories({ owner, repo, tempDir }) {
|
||||
const { token } = await getEnvs();
|
||||
|
||||
console.info('Deleting repository', `${owner}/${repo}`);
|
||||
await fs.remove(tempDir);
|
||||
|
||||
await del(token, `repositories/${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
console.info('Resetting origin repo:', `${owner}/${repo}`);
|
||||
|
||||
const { token } = await getEnvs();
|
||||
|
||||
const pullRequests = await get(token, `repositories/${owner}/${repo}/pullrequests`);
|
||||
const ids = pullRequests.values.map(mr => mr.id);
|
||||
|
||||
console.info('Closing pull requests:', ids);
|
||||
await Promise.all(
|
||||
ids.map(id => post(token, `repositories/${owner}/${repo}/pullrequests/${id}/decline`)),
|
||||
);
|
||||
const branches = await get(token, `repositories/${owner}/${repo}/refs/branches`);
|
||||
const toDelete = branches.values.filter(b => b.name !== 'main').map(b => b.name);
|
||||
|
||||
console.info('Deleting branches', toDelete);
|
||||
await Promise.all(
|
||||
toDelete.map(branch => del(token, `repositories/${owner}/${repo}/refs/branches/${branch}`)),
|
||||
);
|
||||
|
||||
console.info('Resetting main');
|
||||
const git = getGitClient(tempDir);
|
||||
await git.push(['--force', 'origin', 'main']);
|
||||
console.info('Done resetting origin repo:', `${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async function resetRepositories({ owner, repo, tempDir }) {
|
||||
await resetOriginRepo({ owner, repo, tempDir });
|
||||
}
|
||||
|
||||
async function setupBitBucket(options) {
|
||||
const { lfs = false, ...rest } = options;
|
||||
|
||||
console.info('Running tests - live data will be used!');
|
||||
const [user, repoData] = await Promise.all([getUser(), prepareTestBitBucketRepo({ lfs })]);
|
||||
|
||||
console.info('Updating config...');
|
||||
await updateConfig(config => {
|
||||
merge(config, rest, {
|
||||
backend: {
|
||||
repo: `${repoData.owner}/${repoData.repo}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { ...repoData, user };
|
||||
}
|
||||
|
||||
async function teardownBitBucket(taskData) {
|
||||
await deleteRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setupBitBucketTest(taskData) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const sanitizeString = (str, { owner, repo, token, ownerName }) => {
|
||||
let replaced = str
|
||||
.replace(new RegExp(escapeRegExp(owner), 'g'), BITBUCKET_REPO_OWNER_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(repo), 'g'), BITBUCKET_REPO_NAME_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(token), 'g'), BITBUCKET_REPO_TOKEN_SANITIZED_VALUE)
|
||||
.replace(
|
||||
new RegExp('https://secure.gravatar.+?/u/.+?v=\\d', 'g'),
|
||||
`${FAKE_OWNER_USER.links.avatar.href}`,
|
||||
)
|
||||
.replace(new RegExp(/\?token=.+?&/g), 'token=fakeToken&')
|
||||
.replace(new RegExp(/&client=.+?&/g), 'client=fakeClient&');
|
||||
|
||||
if (ownerName) {
|
||||
replaced = replaced.replace(
|
||||
new RegExp(escapeRegExp(ownerName), 'g'),
|
||||
FAKE_OWNER_USER.display_name,
|
||||
);
|
||||
}
|
||||
|
||||
return replaced;
|
||||
};
|
||||
|
||||
const transformRecordedData = (expectation, toSanitize) => {
|
||||
const requestBodySanitizer = httpRequest => {
|
||||
let body;
|
||||
if (httpRequest.body && httpRequest.body.type === 'JSON' && httpRequest.body.json) {
|
||||
const bodyObject = JSON.parse(httpRequest.body.json);
|
||||
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.json;
|
||||
}
|
||||
} else if (httpRequest.body && httpRequest.body.type === 'STRING' && httpRequest.body.string) {
|
||||
body = httpRequest.body.string;
|
||||
} else if (
|
||||
httpRequest.body &&
|
||||
httpRequest.body.type === 'BINARY' &&
|
||||
httpRequest.body.base64Bytes
|
||||
) {
|
||||
body = {
|
||||
encoding: 'base64',
|
||||
content: httpRequest.body.base64Bytes,
|
||||
contentType: httpRequest.body.contentType,
|
||||
};
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
const responseBodySanitizer = (httpRequest, httpResponse) => {
|
||||
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) {
|
||||
if (httpResponse.body && httpResponse.body.type === 'JSON' && httpResponse.body.json) {
|
||||
responseBody = JSON.stringify(httpResponse.body.json);
|
||||
} else {
|
||||
responseBody = httpResponse.body;
|
||||
}
|
||||
}
|
||||
|
||||
// replace recorded user with fake one
|
||||
if (
|
||||
responseBody &&
|
||||
httpRequest.path === '/2.0/user' &&
|
||||
httpRequest.headers.host.includes('api.bitbucket.org')
|
||||
) {
|
||||
responseBody = JSON.stringify(FAKE_OWNER_USER);
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
};
|
||||
|
||||
return transformData(expectation, requestBodySanitizer, responseBodySanitizer);
|
||||
};
|
||||
|
||||
async function teardownBitBucketTest(taskData) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupBitBucket,
|
||||
teardownBitBucket,
|
||||
setupBitBucketTest,
|
||||
teardownBitBucketTest,
|
||||
};
|
80
cypress/plugins/common.js
Normal file
@ -0,0 +1,80 @@
|
||||
const path = require('path');
|
||||
const { default: simpleGit } = require('simple-git');
|
||||
|
||||
const GIT_SSH_COMMAND = 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no';
|
||||
const GIT_SSL_NO_VERIFY = true;
|
||||
|
||||
const getExpectationsFilename = taskData => {
|
||||
const { spec, testName } = taskData;
|
||||
const basename = `${spec}__${testName}`;
|
||||
const fixtures = path.join(__dirname, '..', 'fixtures');
|
||||
const filename = path.join(fixtures, `${basename}.json`);
|
||||
|
||||
return filename;
|
||||
};
|
||||
|
||||
const HEADERS_TO_IGNORE = [
|
||||
'Date',
|
||||
'X-RateLimit-Remaining',
|
||||
'X-RateLimit-Reset',
|
||||
'ETag',
|
||||
'Last-Modified',
|
||||
'X-GitHub-Request-Id',
|
||||
'X-NF-Request-ID',
|
||||
'X-Request-Id',
|
||||
'X-Runtime',
|
||||
'RateLimit-Limit',
|
||||
'RateLimit-Observed',
|
||||
'RateLimit-Remaining',
|
||||
'RateLimit-Reset',
|
||||
'RateLimit-ResetTime',
|
||||
'GitLab-LB',
|
||||
].map(h => h.toLowerCase());
|
||||
|
||||
const transformRecordedData = (expectation, requestBodySanitizer, responseBodySanitizer) => {
|
||||
const { httpRequest, httpResponse } = expectation;
|
||||
|
||||
const responseHeaders = {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
};
|
||||
|
||||
Object.keys(httpResponse.headers)
|
||||
.filter(key => !HEADERS_TO_IGNORE.includes(key.toLowerCase()))
|
||||
.forEach(key => {
|
||||
responseHeaders[key] = httpResponse.headers[key][0];
|
||||
});
|
||||
|
||||
let queryString;
|
||||
if (httpRequest.queryStringParameters) {
|
||||
const { queryStringParameters } = httpRequest;
|
||||
|
||||
queryString = Object.keys(queryStringParameters)
|
||||
.map(key => `${key}=${queryStringParameters[key]}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
const body = requestBodySanitizer(httpRequest);
|
||||
const responseBody = responseBodySanitizer(httpRequest, httpResponse);
|
||||
|
||||
const cypressRouteOptions = {
|
||||
body,
|
||||
method: httpRequest.method,
|
||||
url: queryString ? `${httpRequest.path}?${queryString}` : httpRequest.path,
|
||||
headers: responseHeaders,
|
||||
response: responseBody,
|
||||
status: httpResponse.statusCode,
|
||||
};
|
||||
|
||||
return cypressRouteOptions;
|
||||
};
|
||||
|
||||
function getGitClient(repoDir) {
|
||||
const git = simpleGit(repoDir).env({ ...process.env, GIT_SSH_COMMAND, GIT_SSL_NO_VERIFY });
|
||||
return git;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getExpectationsFilename,
|
||||
transformRecordedData,
|
||||
getGitClient,
|
||||
};
|
284
cypress/plugins/gitGateway.js
Normal file
@ -0,0 +1,284 @@
|
||||
const fetch = require('node-fetch');
|
||||
const {
|
||||
transformRecordedData: transformGitHub,
|
||||
setupGitHub,
|
||||
teardownGitHub,
|
||||
setupGitHubTest,
|
||||
teardownGitHubTest,
|
||||
} = require('./github');
|
||||
const {
|
||||
transformRecordedData: transformGitLab,
|
||||
setupGitLab,
|
||||
teardownGitLab,
|
||||
setupGitLabTest,
|
||||
teardownGitLabTest,
|
||||
} = require('./gitlab');
|
||||
const { getGitClient } = require('./common');
|
||||
|
||||
function getEnvs() {
|
||||
const {
|
||||
NETLIFY_API_TOKEN: netlifyApiToken,
|
||||
GITHUB_REPO_TOKEN: githubToken,
|
||||
GITLAB_REPO_TOKEN: gitlabToken,
|
||||
NETLIFY_INSTALLATION_ID: installationId,
|
||||
} = process.env;
|
||||
if (!netlifyApiToken) {
|
||||
throw new Error(
|
||||
'Please set NETLIFY_API_TOKEN, GITHUB_REPO_TOKEN, GITLAB_REPO_TOKEN, NETLIFY_INSTALLATION_ID environment variables',
|
||||
);
|
||||
}
|
||||
return { netlifyApiToken, githubToken, gitlabToken, 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}`,
|
||||
},
|
||||
...(payload ? { 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, provider, token, repo) {
|
||||
return post(netlifyApiToken, `sites/${siteId}/services/git/instances`, {
|
||||
[provider]: {
|
||||
repo,
|
||||
access_token: token,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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.info('Deploy finished for site:', siteId);
|
||||
return;
|
||||
}
|
||||
console.info('Waiting on deploy of site:', siteId);
|
||||
await new Promise(resolve => setTimeout(resolve, 30 * 1000));
|
||||
}
|
||||
console.info('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.info('User created successfully');
|
||||
} else {
|
||||
throw new Error('Failed to create user');
|
||||
}
|
||||
}
|
||||
|
||||
const netlifySiteURL = 'https://fake-site-url.netlify.com/';
|
||||
const email = 'static@p-m.si';
|
||||
const password = '12345678';
|
||||
const backendName = 'git-gateway';
|
||||
|
||||
const methods = {
|
||||
github: {
|
||||
setup: setupGitHub,
|
||||
teardown: teardownGitHub,
|
||||
setupTest: setupGitHubTest,
|
||||
teardownTest: teardownGitHubTest,
|
||||
transformData: transformGitHub,
|
||||
createSite: (netlifyApiToken, result) => {
|
||||
const { installationId } = getEnvs();
|
||||
return createSite(netlifyApiToken, {
|
||||
repo: {
|
||||
provider: 'github',
|
||||
installation_id: installationId,
|
||||
repo: `${result.owner}/${result.repo}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
token: () => getEnvs().githubToken,
|
||||
},
|
||||
gitlab: {
|
||||
setup: setupGitLab,
|
||||
teardown: teardownGitLab,
|
||||
setupTest: setupGitLabTest,
|
||||
teardownTest: teardownGitLabTest,
|
||||
transformData: transformGitLab,
|
||||
createSite: async (netlifyApiToken, result) => {
|
||||
const { id, public_key } = await post(netlifyApiToken, 'deploy_keys');
|
||||
const { gitlabToken } = getEnvs();
|
||||
const project = `${result.owner}/${result.repo}`;
|
||||
await fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/deploy_keys`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${gitlabToken}`,
|
||||
},
|
||||
body: JSON.stringify({ title: 'Netlify Deploy Key', key: public_key, can_push: false }),
|
||||
}).then(res => res.json());
|
||||
|
||||
const site = await createSite(netlifyApiToken, {
|
||||
account_slug: result.owner,
|
||||
repo: {
|
||||
provider: 'gitlab',
|
||||
repo: `${result.owner}/${result.repo}`,
|
||||
deploy_key_id: id,
|
||||
},
|
||||
});
|
||||
|
||||
await fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/hooks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${gitlabToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: 'https://api.netlify.com/hooks/gitlab',
|
||||
push_events: true,
|
||||
merge_requests_events: true,
|
||||
enable_ssl_verification: true,
|
||||
}),
|
||||
}).then(res => res.json());
|
||||
|
||||
return site;
|
||||
},
|
||||
token: () => getEnvs().gitlabToken,
|
||||
},
|
||||
};
|
||||
|
||||
async function setupGitGateway(options) {
|
||||
const { provider, ...rest } = options;
|
||||
const result = await methods[provider].setup(rest);
|
||||
|
||||
const { netlifyApiToken } = getEnvs();
|
||||
|
||||
console.info(`Creating Netlify Site for provider: ${provider}`);
|
||||
let site_id, ssl_url;
|
||||
try {
|
||||
({ site_id, ssl_url } = await methods[provider].createSite(netlifyApiToken, result));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.info('Enabling identity for site:', site_id);
|
||||
await enableIdentity(netlifyApiToken, site_id);
|
||||
|
||||
console.info('Enabling git gateway for site:', site_id);
|
||||
const token = methods[provider].token();
|
||||
await enableGitGateway(
|
||||
netlifyApiToken,
|
||||
site_id,
|
||||
provider,
|
||||
token,
|
||||
`${result.owner}/${result.repo}`,
|
||||
);
|
||||
|
||||
console.info('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', 'main');
|
||||
|
||||
await waitForDeploys(netlifyApiToken, site_id);
|
||||
console.info('Creating user for site:', site_id, 'with email:', email);
|
||||
|
||||
try {
|
||||
await createUser(netlifyApiToken, ssl_url, email, password);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
user: {
|
||||
...result.user,
|
||||
backendName,
|
||||
netlifySiteURL: ssl_url,
|
||||
email,
|
||||
password,
|
||||
},
|
||||
site_id,
|
||||
ssl_url,
|
||||
provider,
|
||||
};
|
||||
}
|
||||
|
||||
async function teardownGitGateway(taskData) {
|
||||
const { netlifyApiToken } = getEnvs();
|
||||
const { site_id } = taskData;
|
||||
console.info('Deleting Netlify site:', site_id);
|
||||
await del(netlifyApiToken, `sites/${site_id}`);
|
||||
|
||||
return methods[taskData.provider].teardown(taskData);
|
||||
}
|
||||
|
||||
async function setupGitGatewayTest(taskData) {
|
||||
return methods[taskData.provider].setupTest(taskData);
|
||||
}
|
||||
|
||||
async function teardownGitGatewayTest(taskData) {
|
||||
return methods[taskData.provider].teardownTest(taskData, {});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupGitGateway,
|
||||
teardownGitGateway,
|
||||
setupGitGatewayTest,
|
||||
teardownGitGatewayTest,
|
||||
};
|
426
cypress/plugins/github.js
Normal file
@ -0,0 +1,426 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const {
|
||||
getExpectationsFilename,
|
||||
transformRecordedData: transformData,
|
||||
getGitClient,
|
||||
} = require('./common');
|
||||
const { updateConfig } = require('../utils/config');
|
||||
const { escapeRegExp } = require('../utils/regexp');
|
||||
const { merge } = require('lodash');
|
||||
|
||||
const GITHUB_REPO_OWNER_SANITIZED_VALUE = 'owner';
|
||||
const GITHUB_REPO_NAME_SANITIZED_VALUE = 'repo';
|
||||
const GITHUB_REPO_TOKEN_SANITIZED_VALUE = 'fakeToken';
|
||||
const GITHUB_OPEN_AUTHORING_OWNER_SANITIZED_VALUE = 'forkOwner';
|
||||
const GITHUB_OPEN_AUTHORING_TOKEN_SANITIZED_VALUE = 'fakeForkToken';
|
||||
|
||||
const FAKE_OWNER_USER = {
|
||||
login: 'owner',
|
||||
id: 1,
|
||||
avatar_url: 'https://avatars1.githubusercontent.com/u/7892489?v=4',
|
||||
name: 'owner',
|
||||
};
|
||||
|
||||
const FAKE_FORK_OWNER_USER = {
|
||||
login: 'forkOwner',
|
||||
id: 2,
|
||||
avatar_url: 'https://avatars1.githubusercontent.com/u/9919?s=200&v=4',
|
||||
name: 'forkOwner',
|
||||
};
|
||||
|
||||
function getGitHubClient(token) {
|
||||
const client = new Octokit({
|
||||
auth: `token ${token}`,
|
||||
baseUrl: 'https://api.github.com',
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
function getEnvs() {
|
||||
const {
|
||||
GITHUB_REPO_OWNER: owner,
|
||||
GITHUB_REPO_NAME: repo,
|
||||
GITHUB_REPO_TOKEN: token,
|
||||
GITHUB_OPEN_AUTHORING_OWNER: forkOwner,
|
||||
GITHUB_OPEN_AUTHORING_TOKEN: forkToken,
|
||||
} = process.env;
|
||||
if (!owner || !repo || !token || !forkOwner || !forkToken) {
|
||||
throw new Error(
|
||||
'Please set GITHUB_REPO_OWNER, GITHUB_REPO_NAME, GITHUB_REPO_TOKEN, GITHUB_OPEN_AUTHORING_OWNER, GITHUB_OPEN_AUTHORING_TOKEN environment variables',
|
||||
);
|
||||
}
|
||||
return { owner, repo, token, forkOwner, forkToken };
|
||||
}
|
||||
|
||||
async function prepareTestGitHubRepo() {
|
||||
const { owner, repo, token } = getEnvs();
|
||||
|
||||
// postfix a random string to avoid collisions
|
||||
const postfix = Math.random().toString(32).slice(2);
|
||||
const testRepoName = `${repo}-${Date.now()}-${postfix}`;
|
||||
|
||||
const client = getGitHubClient(token);
|
||||
|
||||
console.info('Creating repository', testRepoName);
|
||||
await client.repos.createForAuthenticatedUser({
|
||||
name: testRepoName,
|
||||
});
|
||||
|
||||
const tempDir = path.join('.temp', testRepoName);
|
||||
await fs.remove(tempDir);
|
||||
let git = getGitClient();
|
||||
|
||||
const repoUrl = `git@github.com:${owner}/${repo}.git`;
|
||||
|
||||
console.info('Cloning repository', repoUrl);
|
||||
await git.clone(repoUrl, tempDir);
|
||||
git = getGitClient(tempDir);
|
||||
|
||||
console.info('Pushing to new repository', testRepoName);
|
||||
|
||||
await git.removeRemote('origin');
|
||||
await git.addRemote(
|
||||
'origin',
|
||||
`https://${token}:x-oauth-basic@github.com/${owner}/${testRepoName}`,
|
||||
);
|
||||
await git.push(['-u', 'origin', 'main']);
|
||||
|
||||
return { owner, repo: testRepoName, tempDir };
|
||||
}
|
||||
|
||||
async function getAuthenticatedUser(token) {
|
||||
const client = getGitHubClient(token);
|
||||
const { data: user } = await client.users.getAuthenticated();
|
||||
return { ...user, token, backendName: 'github' };
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
const { token } = getEnvs();
|
||||
return getAuthenticatedUser(token);
|
||||
}
|
||||
|
||||
async function getForkUser() {
|
||||
const { forkToken } = getEnvs();
|
||||
return getAuthenticatedUser(forkToken);
|
||||
}
|
||||
|
||||
async function deleteRepositories({ owner, repo, tempDir }) {
|
||||
const { forkOwner, token, forkToken } = getEnvs();
|
||||
|
||||
const errorHandler = e => {
|
||||
if (e.status !== 404) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
console.info('Deleting repository', `${owner}/${repo}`);
|
||||
await fs.remove(tempDir);
|
||||
|
||||
let client = getGitHubClient(token);
|
||||
await client.repos
|
||||
.delete({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
.catch(errorHandler);
|
||||
|
||||
console.info('Deleting forked repository', `${forkOwner}/${repo}`);
|
||||
client = getGitHubClient(forkToken);
|
||||
await client.repos
|
||||
.delete({
|
||||
owner: forkOwner,
|
||||
repo,
|
||||
})
|
||||
.catch(errorHandler);
|
||||
}
|
||||
|
||||
async function batchRequests(items, batchSize, func) {
|
||||
while (items.length > 0) {
|
||||
const batch = items.splice(0, batchSize);
|
||||
await Promise.all(batch.map(func));
|
||||
await new Promise(resolve => setTimeout(resolve, 2500));
|
||||
}
|
||||
}
|
||||
|
||||
async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
console.info('Resetting origin repo:', `${owner}/${repo}`);
|
||||
const { token } = getEnvs();
|
||||
const client = getGitHubClient(token);
|
||||
|
||||
const { data: prs } = await client.pulls.list({
|
||||
repo,
|
||||
owner,
|
||||
state: 'open',
|
||||
});
|
||||
const numbers = prs.map(pr => pr.number);
|
||||
console.info('Closing prs:', numbers);
|
||||
|
||||
await batchRequests(numbers, 10, async pull_number => {
|
||||
await client.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
state: 'closed',
|
||||
});
|
||||
});
|
||||
|
||||
const { data: branches } = await client.repos.listBranches({ owner, repo });
|
||||
const refs = branches.filter(b => b.name !== 'main').map(b => `heads/${b.name}`);
|
||||
|
||||
console.info('Deleting refs', refs);
|
||||
|
||||
await batchRequests(refs, 10, async ref => {
|
||||
await client.git.deleteRef({
|
||||
owner,
|
||||
repo,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
|
||||
console.info('Resetting main');
|
||||
const git = getGitClient(tempDir);
|
||||
await git.push(['--force', 'origin', 'main']);
|
||||
console.info('Done resetting origin repo:', `${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async function resetForkedRepo({ repo }) {
|
||||
const { forkToken, forkOwner } = getEnvs();
|
||||
const client = getGitHubClient(forkToken);
|
||||
|
||||
const { data: repos } = await client.repos.list();
|
||||
if (repos.some(r => r.name === repo)) {
|
||||
console.info('Resetting forked repo:', `${forkOwner}/${repo}`);
|
||||
const { data: branches } = await client.repos.listBranches({ owner: forkOwner, repo });
|
||||
const refs = branches.filter(b => b.name !== 'main').map(b => `heads/${b.name}`);
|
||||
|
||||
console.info('Deleting refs', refs);
|
||||
await Promise.all(
|
||||
refs.map(ref =>
|
||||
client.git.deleteRef({
|
||||
owner: forkOwner,
|
||||
repo,
|
||||
ref,
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.info('Done resetting forked repo:', `${forkOwner}/${repo}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetRepositories({ owner, repo, tempDir }) {
|
||||
await resetOriginRepo({ owner, repo, tempDir });
|
||||
await resetForkedRepo({ repo });
|
||||
}
|
||||
|
||||
async function setupGitHub(options) {
|
||||
console.info('Running tests - live data will be used!');
|
||||
const [user, forkUser, repoData] = await Promise.all([
|
||||
getUser(),
|
||||
getForkUser(),
|
||||
prepareTestGitHubRepo(),
|
||||
]);
|
||||
|
||||
await updateConfig(config => {
|
||||
merge(config, options, {
|
||||
backend: {
|
||||
repo: `${repoData.owner}/${repoData.repo}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { ...repoData, user, forkUser };
|
||||
}
|
||||
|
||||
async function teardownGitHub(taskData) {
|
||||
await deleteRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setupGitHubTest(taskData) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const sanitizeString = (
|
||||
str,
|
||||
{ owner, repo, token, forkOwner, forkToken, ownerName, forkOwnerName },
|
||||
) => {
|
||||
let replaced = str
|
||||
.replace(new RegExp(escapeRegExp(forkOwner), 'g'), GITHUB_OPEN_AUTHORING_OWNER_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(forkToken), 'g'), GITHUB_OPEN_AUTHORING_TOKEN_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(owner), 'g'), GITHUB_REPO_OWNER_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(repo), 'g'), GITHUB_REPO_NAME_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(token), 'g'), GITHUB_REPO_TOKEN_SANITIZED_VALUE)
|
||||
.replace(
|
||||
new RegExp('https://avatars\\d+\\.githubusercontent\\.com/u/\\d+?\\?v=\\d', 'g'),
|
||||
`${FAKE_OWNER_USER.avatar_url}`,
|
||||
);
|
||||
|
||||
if (ownerName) {
|
||||
replaced = replaced.replace(new RegExp(escapeRegExp(ownerName), 'g'), FAKE_OWNER_USER.name);
|
||||
}
|
||||
|
||||
if (forkOwnerName) {
|
||||
replaced = replaced.replace(
|
||||
new RegExp(escapeRegExp(forkOwnerName), 'g'),
|
||||
FAKE_FORK_OWNER_USER.name,
|
||||
);
|
||||
}
|
||||
|
||||
return replaced;
|
||||
};
|
||||
|
||||
const transformRecordedData = (expectation, toSanitize) => {
|
||||
const requestBodySanitizer = httpRequest => {
|
||||
let body;
|
||||
if (httpRequest.body && httpRequest.body.type === 'JSON' && httpRequest.body.json) {
|
||||
const bodyObject =
|
||||
typeof httpRequest.body.json === 'string'
|
||||
? JSON.parse(httpRequest.body.json)
|
||||
: httpRequest.body.json;
|
||||
|
||||
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 = JSON.stringify(bodyObject);
|
||||
}
|
||||
} else if (httpRequest.body && httpRequest.body.type === 'STRING' && httpRequest.body.string) {
|
||||
body = httpRequest.body.string;
|
||||
} else if (httpRequest.body) {
|
||||
const str =
|
||||
typeof httpRequest.body !== 'string' ? JSON.stringify(httpRequest.body) : httpRequest.body;
|
||||
body = sanitizeString(str, toSanitize);
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
const responseBodySanitizer = (httpRequest, httpResponse) => {
|
||||
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 && httpResponse.body.json) {
|
||||
responseBody = JSON.stringify(httpResponse.body.json);
|
||||
} else {
|
||||
responseBody =
|
||||
typeof httpResponse.body === 'string'
|
||||
? httpResponse.body
|
||||
: httpResponse.body && JSON.stringify(httpResponse.body);
|
||||
}
|
||||
|
||||
// replace recorded user with fake one
|
||||
if (
|
||||
responseBody &&
|
||||
httpRequest.path === '/user' &&
|
||||
httpRequest.headers.host.includes('api.github.com')
|
||||
) {
|
||||
const parsed = JSON.parse(responseBody);
|
||||
if (parsed.login === toSanitize.forkOwner) {
|
||||
responseBody = JSON.stringify(FAKE_FORK_OWNER_USER);
|
||||
} else {
|
||||
responseBody = JSON.stringify(FAKE_OWNER_USER);
|
||||
}
|
||||
}
|
||||
return responseBody;
|
||||
};
|
||||
|
||||
const cypressRouteOptions = transformData(
|
||||
expectation,
|
||||
requestBodySanitizer,
|
||||
responseBodySanitizer,
|
||||
);
|
||||
|
||||
return cypressRouteOptions;
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
transformRecordedData,
|
||||
};
|
||||
|
||||
async function teardownGitHubTest(taskData, { transformRecordedData } = defaultOptions) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function seedGitHubRepo(taskData) {
|
||||
const { owner, token } = getEnvs();
|
||||
|
||||
const client = getGitHubClient(token);
|
||||
const repo = taskData.repo;
|
||||
|
||||
try {
|
||||
console.info('Getting main branch');
|
||||
const { data: main } = await client.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: 'main',
|
||||
});
|
||||
|
||||
const prCount = 120;
|
||||
const prs = new Array(prCount).fill(0).map((v, i) => i);
|
||||
const batchSize = 5;
|
||||
await batchRequests(prs, batchSize, async i => {
|
||||
const branch = `seed_branch_${i}`;
|
||||
console.info(`Creating branch ${branch}`);
|
||||
await client.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/heads/${branch}`,
|
||||
sha: main.commit.sha,
|
||||
});
|
||||
|
||||
const path = `seed/file_${i}`;
|
||||
console.info(`Creating file ${path}`);
|
||||
await client.repos.createOrUpdateFile({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
content: Buffer.from(`Seed File ${i}`).toString('base64'),
|
||||
message: `Create seed file ${i}`,
|
||||
path,
|
||||
});
|
||||
|
||||
const title = `Non CMS Pull Request ${i}`;
|
||||
console.info(`Creating PR ${title}`);
|
||||
await client.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
base: 'main',
|
||||
head: branch,
|
||||
title,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transformRecordedData,
|
||||
setupGitHub,
|
||||
teardownGitHub,
|
||||
setupGitHubTest,
|
||||
teardownGitHubTest,
|
||||
seedGitHubRepo,
|
||||
};
|
275
cypress/plugins/gitlab.js
Normal file
@ -0,0 +1,275 @@
|
||||
const { Gitlab } = require('gitlab');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { updateConfig } = require('../utils/config');
|
||||
const { escapeRegExp } = require('../utils/regexp');
|
||||
const {
|
||||
getExpectationsFilename,
|
||||
transformRecordedData: transformData,
|
||||
getGitClient,
|
||||
} = require('./common');
|
||||
const { merge } = require('lodash');
|
||||
|
||||
const GITLAB_REPO_OWNER_SANITIZED_VALUE = 'owner';
|
||||
const GITLAB_REPO_NAME_SANITIZED_VALUE = 'repo';
|
||||
const GITLAB_REPO_TOKEN_SANITIZED_VALUE = 'fakeToken';
|
||||
|
||||
const FAKE_OWNER_USER = {
|
||||
id: 1,
|
||||
name: 'owner',
|
||||
username: 'owner',
|
||||
avatar_url: 'https://avatars1.githubusercontent.com/u/7892489?v=4',
|
||||
email: 'owner@email.com',
|
||||
login: 'owner',
|
||||
};
|
||||
|
||||
function getGitLabClient(token) {
|
||||
const client = new Gitlab({
|
||||
token,
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
function getEnvs() {
|
||||
const {
|
||||
GITLAB_REPO_OWNER: owner,
|
||||
GITLAB_REPO_NAME: repo,
|
||||
GITLAB_REPO_TOKEN: token,
|
||||
} = process.env;
|
||||
if (!owner || !repo || !token) {
|
||||
throw new Error(
|
||||
'Please set GITLAB_REPO_OWNER, GITLAB_REPO_NAME, GITLAB_REPO_TOKEN environment variables',
|
||||
);
|
||||
}
|
||||
return { owner, repo, token };
|
||||
}
|
||||
|
||||
async function prepareTestGitLabRepo() {
|
||||
const { owner, repo, token } = getEnvs();
|
||||
|
||||
// postfix a random string to avoid collisions
|
||||
const postfix = Math.random().toString(32).slice(2);
|
||||
const testRepoName = `${repo}-${Date.now()}-${postfix}`;
|
||||
|
||||
const client = getGitLabClient(token);
|
||||
|
||||
console.info('Creating repository', testRepoName);
|
||||
await client.Projects.create({
|
||||
name: testRepoName,
|
||||
lfs_enabled: false,
|
||||
});
|
||||
|
||||
const tempDir = path.join('.temp', testRepoName);
|
||||
await fs.remove(tempDir);
|
||||
let git = getGitClient();
|
||||
|
||||
const repoUrl = `git@gitlab.com:${owner}/${repo}.git`;
|
||||
|
||||
console.info('Cloning repository', repoUrl);
|
||||
await git.clone(repoUrl, tempDir);
|
||||
git = getGitClient(tempDir);
|
||||
|
||||
console.info('Pushing to new repository', testRepoName);
|
||||
|
||||
await git.removeRemote('origin');
|
||||
await git.addRemote('origin', `https://oauth2:${token}@gitlab.com/${owner}/${testRepoName}`);
|
||||
await git.push(['-u', 'origin', 'main']);
|
||||
|
||||
await client.ProtectedBranches.unprotect(`${owner}/${testRepoName}`, 'main');
|
||||
|
||||
return { owner, repo: testRepoName, tempDir };
|
||||
}
|
||||
|
||||
async function getAuthenticatedUser(token) {
|
||||
const client = getGitLabClient(token);
|
||||
const user = await client.Users.current();
|
||||
return { ...user, token, backendName: 'gitlab' };
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
const { token } = getEnvs();
|
||||
return getAuthenticatedUser(token);
|
||||
}
|
||||
|
||||
async function deleteRepositories({ owner, repo, tempDir }) {
|
||||
const { token } = getEnvs();
|
||||
|
||||
const errorHandler = e => {
|
||||
if (e.status !== 404) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
console.info('Deleting repository', `${owner}/${repo}`);
|
||||
await fs.remove(tempDir);
|
||||
|
||||
const client = getGitLabClient(token);
|
||||
await client.Projects.remove(`${owner}/${repo}`).catch(errorHandler);
|
||||
}
|
||||
|
||||
async function resetOriginRepo({ owner, repo, tempDir }) {
|
||||
console.info('Resetting origin repo:', `${owner}/${repo}`);
|
||||
|
||||
const { token } = getEnvs();
|
||||
const client = getGitLabClient(token);
|
||||
|
||||
const projectId = `${owner}/${repo}`;
|
||||
const mergeRequests = await client.MergeRequests.all({
|
||||
projectId,
|
||||
state: 'opened',
|
||||
});
|
||||
const ids = mergeRequests.map(mr => mr.iid);
|
||||
console.info('Closing merge requests:', ids);
|
||||
await Promise.all(
|
||||
ids.map(id => client.MergeRequests.edit(projectId, id, { state_event: 'close' })),
|
||||
);
|
||||
const branches = await client.Branches.all(projectId);
|
||||
const toDelete = branches.filter(b => b.name !== 'main').map(b => b.name);
|
||||
|
||||
console.info('Deleting branches', toDelete);
|
||||
await Promise.all(toDelete.map(branch => client.Branches.remove(projectId, branch)));
|
||||
|
||||
console.info('Resetting main');
|
||||
const git = getGitClient(tempDir);
|
||||
await git.push(['--force', 'origin', 'main']);
|
||||
console.info('Done resetting origin repo:', `${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async function resetRepositories({ owner, repo, tempDir }) {
|
||||
await resetOriginRepo({ owner, repo, tempDir });
|
||||
}
|
||||
|
||||
async function setupGitLab(options) {
|
||||
console.info('Running tests - live data will be used!');
|
||||
const [user, repoData] = await Promise.all([getUser(), prepareTestGitLabRepo()]);
|
||||
|
||||
await updateConfig(config => {
|
||||
merge(config, options, {
|
||||
backend: {
|
||||
repo: `${repoData.owner}/${repoData.repo}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return { ...repoData, user };
|
||||
}
|
||||
|
||||
async function teardownGitLab(taskData) {
|
||||
await deleteRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setupGitLabTest(taskData) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const sanitizeString = (str, { owner, repo, token, ownerName }) => {
|
||||
let replaced = str
|
||||
.replace(new RegExp(escapeRegExp(owner), 'g'), GITLAB_REPO_OWNER_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(repo), 'g'), GITLAB_REPO_NAME_SANITIZED_VALUE)
|
||||
.replace(new RegExp(escapeRegExp(token), 'g'), GITLAB_REPO_TOKEN_SANITIZED_VALUE)
|
||||
.replace(
|
||||
new RegExp('https://secure.gravatar.+?/u/.+?v=\\d', 'g'),
|
||||
`${FAKE_OWNER_USER.avatar_url}`,
|
||||
);
|
||||
|
||||
if (ownerName) {
|
||||
replaced = replaced.replace(new RegExp(escapeRegExp(ownerName), 'g'), FAKE_OWNER_USER.name);
|
||||
}
|
||||
|
||||
return replaced;
|
||||
};
|
||||
|
||||
const transformRecordedData = (expectation, toSanitize) => {
|
||||
const requestBodySanitizer = httpRequest => {
|
||||
let body;
|
||||
if (httpRequest.body && httpRequest.body.type === 'JSON' && httpRequest.body.json) {
|
||||
const bodyObject =
|
||||
typeof httpRequest.body.json === 'string'
|
||||
? JSON.parse(httpRequest.body.json)
|
||||
: httpRequest.body.json;
|
||||
|
||||
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 = JSON.stringify(bodyObject);
|
||||
}
|
||||
} else if (httpRequest.body && httpRequest.body.type === 'STRING' && httpRequest.body.string) {
|
||||
body = sanitizeString(httpRequest.body.string, toSanitize);
|
||||
} else if (httpRequest.body) {
|
||||
const str =
|
||||
typeof httpRequest.body !== 'string' ? JSON.stringify(httpRequest.body) : httpRequest.body;
|
||||
body = sanitizeString(str, toSanitize);
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
const responseBodySanitizer = (httpRequest, httpResponse) => {
|
||||
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 && httpResponse.body.json) {
|
||||
responseBody = JSON.stringify(httpResponse.body.json);
|
||||
} else {
|
||||
responseBody =
|
||||
typeof httpResponse.body === 'string'
|
||||
? httpResponse.body
|
||||
: httpResponse.body && JSON.stringify(httpResponse.body);
|
||||
}
|
||||
|
||||
// replace recorded user with fake one
|
||||
if (
|
||||
responseBody &&
|
||||
httpRequest.path === '/api/v4/user' &&
|
||||
httpRequest.headers.host.includes('gitlab.com')
|
||||
) {
|
||||
responseBody = JSON.stringify(FAKE_OWNER_USER);
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
};
|
||||
|
||||
const cypressRouteOptions = transformData(
|
||||
expectation,
|
||||
requestBodySanitizer,
|
||||
responseBodySanitizer,
|
||||
);
|
||||
|
||||
return cypressRouteOptions;
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
transformRecordedData,
|
||||
};
|
||||
|
||||
async function teardownGitLabTest(taskData, { transformRecordedData } = defaultOptions) {
|
||||
await resetRepositories(taskData);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transformRecordedData,
|
||||
setupGitLab,
|
||||
teardownGitLab,
|
||||
setupGitLabTest,
|
||||
teardownGitLabTest,
|
||||
};
|
178
cypress/plugins/index.ts
Normal file
@ -0,0 +1,178 @@
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
import 'dotenv/config';
|
||||
|
||||
import { addMatchImageSnapshotPlugin } from '@simonsmith/cypress-image-snapshot/plugin';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
// const { setupGitHub, teardownGitHub, setupGitHubTest, teardownGitHubTest, seedGitHubRepo } = require("./github");
|
||||
// const { setupGitGateway, teardownGitGateway, setupGitGatewayTest, teardownGitGatewayTest } = require("./gitGateway");
|
||||
// const { setupGitLab, teardownGitLab, setupGitLabTest, teardownGitLabTest } = require("./gitlab");
|
||||
// const { setupBitBucket, teardownBitBucket, setupBitBucketTest, teardownBitBucketTest } = require("./bitbucket");
|
||||
// const { setupProxy, teardownProxy, setupProxyTest, teardownProxyTest } = require("./proxy");
|
||||
import { setupTestBackend } from './testBackend';
|
||||
|
||||
import { copyBackendFiles, switchVersion, updateConfig } from '../utils/config';
|
||||
|
||||
import type { Config } from '@staticcms/core/interface';
|
||||
import type {
|
||||
SeedRepoProps,
|
||||
SetupBackendProps,
|
||||
SetupBackendResponse,
|
||||
SetupBackendTestProps,
|
||||
TeardownBackendProps,
|
||||
TeardownBackendTestProps,
|
||||
} from '../interface';
|
||||
|
||||
export default async (on: Cypress.PluginEvents) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
on('task', {
|
||||
async setupBackend({ backend, options }: SetupBackendProps): Promise<SetupBackendResponse> {
|
||||
console.info('Preparing environment for backend', backend);
|
||||
await copyBackendFiles(backend);
|
||||
|
||||
let result = null;
|
||||
switch (backend) {
|
||||
// case "github":
|
||||
// result = await setupGitHub(options);
|
||||
// break;
|
||||
// case "git-gateway":
|
||||
// result = await setupGitGateway(options);
|
||||
// break;
|
||||
// case "gitlab":
|
||||
// result = await setupGitLab(options);
|
||||
// break;
|
||||
// case "bitbucket":
|
||||
// result = await setupBitBucket(options);
|
||||
// break;
|
||||
// case "proxy":
|
||||
// result = await setupProxy(options);
|
||||
// break;
|
||||
case 'test':
|
||||
result = await setupTestBackend(options);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async teardownBackend({ backend }: TeardownBackendProps): Promise<null> {
|
||||
console.info('Tearing down backend', backend);
|
||||
|
||||
switch (
|
||||
backend
|
||||
// case "github":
|
||||
// await teardownGitHub(taskData);
|
||||
// break;
|
||||
// case "git-gateway":
|
||||
// await teardownGitGateway(taskData);
|
||||
// break;
|
||||
// case "gitlab":
|
||||
// await teardownGitLab(taskData);
|
||||
// break;
|
||||
// case "bitbucket":
|
||||
// await teardownBitBucket(taskData);
|
||||
// break;
|
||||
// case "proxy":
|
||||
// await teardownProxy(taskData);
|
||||
// break;
|
||||
) {
|
||||
}
|
||||
|
||||
console.info('Restoring defaults');
|
||||
await copyBackendFiles('test');
|
||||
|
||||
return null;
|
||||
},
|
||||
async setupBackendTest({ backend, testName }: SetupBackendTestProps): Promise<null> {
|
||||
console.info(`Setting up single test '${testName}' for backend`, backend);
|
||||
|
||||
switch (
|
||||
backend
|
||||
// case "github":
|
||||
// await setupGitHubTest(taskData);
|
||||
// break;
|
||||
// case "git-gateway":
|
||||
// await setupGitGatewayTest(taskData);
|
||||
// break;
|
||||
// case "gitlab":
|
||||
// await setupGitLabTest(taskData);
|
||||
// break;
|
||||
// case "bitbucket":
|
||||
// await setupBitBucketTest(taskData);
|
||||
// break;
|
||||
// case "proxy":
|
||||
// await setupProxyTest(taskData);
|
||||
// break;
|
||||
) {
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async teardownBackendTest({ backend, testName }: TeardownBackendTestProps): Promise<null> {
|
||||
console.info(`Tearing down single test '${testName}' for backend`, backend);
|
||||
|
||||
switch (
|
||||
backend
|
||||
// case "github":
|
||||
// await teardownGitHubTest(taskData);
|
||||
// break;
|
||||
// case "git-gateway":
|
||||
// await teardownGitGatewayTest(taskData);
|
||||
// break;
|
||||
// case "gitlab":
|
||||
// await teardownGitLabTest(taskData);
|
||||
// break;
|
||||
// case "bitbucket":
|
||||
// await teardownBitBucketTest(taskData);
|
||||
// break;
|
||||
// case "proxy":
|
||||
// await teardownProxyTest(taskData);
|
||||
// break;
|
||||
) {
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async seedRepo({ backend }: SeedRepoProps): Promise<null> {
|
||||
console.info(`Seeding repository for backend`, backend);
|
||||
|
||||
switch (
|
||||
backend
|
||||
// case "github":
|
||||
// await seedGitHubRepo(taskData);
|
||||
// break;
|
||||
) {
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async switchToVersion(taskData) {
|
||||
const { version } = taskData;
|
||||
|
||||
console.info(`Switching CMS to version '${version}'`);
|
||||
|
||||
await switchVersion(version);
|
||||
|
||||
return null;
|
||||
},
|
||||
async updateConfig(config: Partial<Config>) {
|
||||
await updateConfig(current => {
|
||||
merge(current, config);
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
addMatchImageSnapshotPlugin(on);
|
||||
};
|
105
cypress/plugins/proxy.js
Normal file
@ -0,0 +1,105 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const { merge } = require('lodash');
|
||||
|
||||
const { updateConfig } = require('../utils/config');
|
||||
const { getGitClient } = require('./common');
|
||||
|
||||
const initRepo = async dir => {
|
||||
await fs.remove(dir);
|
||||
await fs.mkdirp(dir);
|
||||
const git = getGitClient(dir);
|
||||
await git.init({ '--initial-branch': 'main' });
|
||||
await git.addConfig('user.email', 'cms-cypress-test@netlify.com');
|
||||
await git.addConfig('user.name', 'cms-cypress-test');
|
||||
|
||||
const readme = 'README.md';
|
||||
await fs.writeFile(path.join(dir, readme), '');
|
||||
await git.add(readme);
|
||||
await git.commit('initial commit', readme, { '--no-verify': true, '--no-gpg-sign': true });
|
||||
};
|
||||
|
||||
const startServer = async (repoDir, mode) => {
|
||||
const tsNode = path.join(__dirname, '..', '..', 'node_modules', '.bin', 'ts-node');
|
||||
const serverDir = path.join(__dirname, '..', '..', 'packages', 'static-server');
|
||||
const distIndex = path.join(serverDir, 'dist', 'index.js');
|
||||
const tsIndex = path.join(serverDir, 'src', 'index.ts');
|
||||
|
||||
const port = 8082;
|
||||
const env = {
|
||||
...process.env,
|
||||
GIT_REPO_DIRECTORY: path.resolve(repoDir),
|
||||
PORT: port,
|
||||
MODE: mode,
|
||||
};
|
||||
|
||||
console.info(`Starting proxy server on port '${port}' with mode ${mode}`);
|
||||
if (await fs.pathExists(distIndex)) {
|
||||
serverProcess = spawn('node', [distIndex], { env, cwd: serverDir });
|
||||
} else {
|
||||
serverProcess = spawn(tsNode, ['--files', tsIndex], { env, cwd: serverDir });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
serverProcess.stdout.on('data', data => {
|
||||
const message = data.toString().trim();
|
||||
console.info(`server:stdout: ${message}`);
|
||||
if (message.includes('Static CMS Proxy Server listening on port')) {
|
||||
resolve(serverProcess);
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', data => {
|
||||
console.error(`server:stderr: ${data.toString().trim()}`);
|
||||
reject(data.toString());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let serverProcess;
|
||||
|
||||
async function setupProxy(options) {
|
||||
const postfix = Math.random().toString(32).slice(2);
|
||||
|
||||
const testRepoName = `proxy-test-repo-${Date.now()}-${postfix}`;
|
||||
const tempDir = path.join('.temp', testRepoName);
|
||||
|
||||
const { mode, ...rest } = options;
|
||||
|
||||
await updateConfig(config => {
|
||||
merge(config, rest);
|
||||
});
|
||||
|
||||
return { tempDir, mode };
|
||||
}
|
||||
|
||||
async function teardownProxy(taskData) {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
}
|
||||
await fs.remove(taskData.tempDir);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setupProxyTest(taskData) {
|
||||
await initRepo(taskData.tempDir);
|
||||
serverProcess = await startServer(taskData.tempDir, taskData.mode);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function teardownProxyTest(taskData) {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
}
|
||||
await fs.remove(taskData.tempDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupProxy,
|
||||
teardownProxy,
|
||||
setupProxyTest,
|
||||
teardownProxyTest,
|
||||
};
|
13
cypress/plugins/testBackend.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import merge from 'lodash/merge';
|
||||
import { updateConfig } from '../utils/config';
|
||||
|
||||
import type { Config } from '@staticcms/core/interface';
|
||||
import type { SetupBackendResponse } from '../interface';
|
||||
|
||||
export async function setupTestBackend(options: Partial<Config>): Promise<SetupBackendResponse> {
|
||||
await updateConfig(current => {
|
||||
merge(current, options);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
41
cypress/run.mjs
Normal file
@ -0,0 +1,41 @@
|
||||
import execa from 'execa';
|
||||
import { globby } from 'globby';
|
||||
|
||||
async function runCypress() {
|
||||
if (process.env.IS_FORK === 'true') {
|
||||
const machineIndex = parseInt(process.env.MACHINE_INDEX);
|
||||
const machineCount = parseInt(process.env.MACHINE_COUNT);
|
||||
const specs = await globby(['cypress/integration/*spec*.js']);
|
||||
const specsPerMachine = Math.floor(specs.length / machineCount);
|
||||
const start = (machineIndex - 1) * specsPerMachine;
|
||||
const machineSpecs =
|
||||
machineIndex === machineCount
|
||||
? specs.slice(start)
|
||||
: specs.slice(start, start + specsPerMachine);
|
||||
|
||||
await execa(
|
||||
'cypress',
|
||||
['run', '--browser', 'chrome', '--headless', '--spec', machineSpecs.join(',')],
|
||||
{ stdio: 'inherit', preferLocal: true },
|
||||
);
|
||||
} else {
|
||||
await execa(
|
||||
'cypress',
|
||||
[
|
||||
'run',
|
||||
'--browser',
|
||||
'chrome',
|
||||
'--headless',
|
||||
'--record',
|
||||
'--parallel',
|
||||
'--ci-build-id',
|
||||
process.env.GITHUB_SHA,
|
||||
'--group',
|
||||
'GitHub CI',
|
||||
],
|
||||
{ stdio: 'inherit', preferLocal: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
runCypress();
|
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 |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 120 KiB |
322
cypress/support/commands.ts
Normal file
@ -0,0 +1,322 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
import { oneLineTrim } from 'common-tags';
|
||||
import { rehype } from 'rehype';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
function runTimes(
|
||||
cyInstance: Cypress.Chainable,
|
||||
fn: (chain: Cypress.Chainable) => Cypress.Chainable,
|
||||
count = 1,
|
||||
) {
|
||||
let chain = cyInstance,
|
||||
i = count;
|
||||
while (i) {
|
||||
i -= 1;
|
||||
chain = fn(chain);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
(
|
||||
[
|
||||
'enter',
|
||||
'backspace',
|
||||
['selectAll', 'selectall'],
|
||||
['up', 'upArrow'],
|
||||
['down', 'downArrow'],
|
||||
['left', 'leftArrow'],
|
||||
['right', 'rightArrow'],
|
||||
] as const
|
||||
).forEach(key => {
|
||||
const [cmd, keyName] = typeof key === 'object' ? key : [key, key];
|
||||
Cypress.Commands.add(cmd, { prevSubject: true }, (subject, { shift, times = 1 } = {}) => {
|
||||
const fn = (chain: Cypress.Chainable) =>
|
||||
chain.type(`${shift ? '{shift}' : ''}{${keyName}}`, { delay: 50 });
|
||||
return runTimes(cy.wrap(subject), fn, times);
|
||||
});
|
||||
});
|
||||
|
||||
// Convert `tab` command from plugin to a child command with `times` support
|
||||
Cypress.Commands.add('tabkey', { prevSubject: true }, (subject, { shift, times } = {}) => {
|
||||
const fn = (chain: Cypress.Chainable) => chain.tab({ shift });
|
||||
cy.wait(100);
|
||||
return runTimes(cy, fn, times).wrap(subject);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => {
|
||||
cy.wrap(subject).trigger('mousedown').then(fn).trigger('mouseup');
|
||||
|
||||
cy.document().trigger('selectionchange');
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('print', { prevSubject: 'optional' }, (subject, str) => {
|
||||
cy.log(str);
|
||||
console.info(`cy.log: ${str}`);
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => {
|
||||
return cy.wrap(subject).selection($el => {
|
||||
if (typeof query === 'string') {
|
||||
const anchorNode = getTextNode($el[0], query);
|
||||
const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode;
|
||||
const anchorOffset = anchorNode?.wholeText.indexOf(query) ?? 0;
|
||||
const focusOffset = endQuery
|
||||
? (focusNode?.wholeText ?? '').indexOf(endQuery) + endQuery.length
|
||||
: anchorOffset + query.length;
|
||||
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
} else if (typeof query === 'object') {
|
||||
const el = $el[0];
|
||||
const anchorNode = getTextNode(el.querySelector(query.anchorQuery));
|
||||
const anchorOffset = query.anchorOffset || 0;
|
||||
const focusNode = query.focusQuery
|
||||
? getTextNode(el.querySelector(query.focusQuery))
|
||||
: anchorNode;
|
||||
const focusOffset = query.focusOffset || 0;
|
||||
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
}
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setCursor', { prevSubject: true }, (subject, query, atStart) => {
|
||||
return cy.wrap(subject).selection($el => {
|
||||
const node = getTextNode($el[0], query);
|
||||
const offset = (node?.wholeText.indexOf(query) ?? 0) + (atStart ? 0 : query.length);
|
||||
const document = node?.ownerDocument;
|
||||
document?.getSelection()?.removeAllRanges();
|
||||
document?.getSelection()?.collapse(node, offset);
|
||||
|
||||
return cy.wrap(subject);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setCursorBefore', { prevSubject: true }, (subject, query) => {
|
||||
cy.wrap(subject).setCursor(query, true);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setCursorAfter', { prevSubject: true }, (subject, query) => {
|
||||
cy.wrap(subject).setCursor(query);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('login', () => {
|
||||
cy.viewport(1200, 1200);
|
||||
cy.visit('/');
|
||||
cy.contains('button', 'Login').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('loginAndNewPost', () => {
|
||||
cy.login();
|
||||
cy.contains('a', 'New Post').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'dragTo',
|
||||
{
|
||||
prevSubject: 'element',
|
||||
},
|
||||
(subject, selector, options) => {
|
||||
cy.wrap(subject, { log: false }).then(subject => {
|
||||
return cy
|
||||
.document()
|
||||
.its('body')
|
||||
.find(selector)
|
||||
.then($el => {
|
||||
const target = $el[0].getBoundingClientRect();
|
||||
const x = Math.floor(target.x + target.width / 2);
|
||||
const y = target.y + 200;
|
||||
|
||||
cy.wrap(subject)
|
||||
.trigger('pointerdown', { force: true })
|
||||
.wait(options?.delay || 100, { log: Boolean(options?.delay) });
|
||||
|
||||
cy.wrap(subject)
|
||||
.trigger('pointermove', {
|
||||
force: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
})
|
||||
.wait(options?.delay || 100, { log: Boolean(options?.delay) });
|
||||
|
||||
cy.wrap(subject)
|
||||
.trigger('pointermove', {
|
||||
force: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
})
|
||||
.wait(options?.delay || 100, { log: Boolean(options?.delay) });
|
||||
|
||||
cy.document().its('body').find(selector).trigger('pointerup', { force: true }).wait(250);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add('clickToolbarButton', (title, { times } = {}) => {
|
||||
const isHeading = title.startsWith('Heading');
|
||||
if (isHeading) {
|
||||
cy.get('button[title="Headings"]').click();
|
||||
}
|
||||
const instance = isHeading ? cy.contains('div', title) : cy.get(`button[title="${title}"]`);
|
||||
const fn = (chain: Cypress.Chainable) => chain.click();
|
||||
// this seems to be the only thing that makes cypress stable(ish)
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(100);
|
||||
return runTimes(instance, fn, times).focused();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insertEditorComponent', (title: string) => {
|
||||
cy.get('button[title="Add Component"]').click();
|
||||
cy.contains('div', title).click().focused();
|
||||
});
|
||||
|
||||
(
|
||||
[
|
||||
['clickHeadingOneButton', 'Heading 1'],
|
||||
['clickHeadingTwoButton', 'Heading 2'],
|
||||
['clickOrderedListButton', 'Numbered List'],
|
||||
['clickUnorderedListButton', 'Bulleted List'],
|
||||
['clickCodeButton', 'Code'],
|
||||
['clickItalicButton', 'Italic'],
|
||||
['clickQuoteButton', 'Quote'],
|
||||
['clickLinkButton', 'Link'],
|
||||
] as const
|
||||
).forEach(([commandName, toolbarButtonName]) => {
|
||||
Cypress.Commands.add(commandName, opts => {
|
||||
return cy.clickToolbarButton(toolbarButtonName, opts);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clickModeToggle', () => {
|
||||
cy.get('.cms-editor-visual').within(() => {
|
||||
cy.get('button[role="switch"]').click().focused();
|
||||
});
|
||||
});
|
||||
|
||||
([['insertCodeBlock', 'Code Block']] as const).forEach(([commandName, componentTitle]) => {
|
||||
Cypress.Commands.add(commandName, () => {
|
||||
return cy.insertEditorComponent(componentTitle);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getMarkdownEditor', () => {
|
||||
return cy.get('[data-slate-editor="true"]');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('confirmMarkdownEditorContent', (expectedDomString: string) => {
|
||||
return cy.getMarkdownEditor().should(([element]) => {
|
||||
// Slate makes the following representations:
|
||||
// - blank line: 2 BOM's + <br>
|
||||
// - blank element (placed inside empty elements): 1 BOM + <br>
|
||||
// - inline element (e.g. link tag <a>) are wrapped with BOM characters (https://github.com/ianstormtaylor/slate/issues/2722)
|
||||
// We replace to represent a blank line as a single <br>, remove the
|
||||
// contents of elements that are actually empty, and remove BOM characters wrapping <a> tags
|
||||
const actualDomString = toPlainTree(element.innerHTML)
|
||||
.replace(/\uFEFF\uFEFF<br>/g, '<br>')
|
||||
.replace(/\uFEFF<br>/g, '')
|
||||
.replace(/\uFEFF<a>/g, '<a>')
|
||||
.replace(/<\/a>\uFEFF/g, '</a>');
|
||||
expect(actualDomString).equals(oneLineTrim(expectedDomString));
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clearMarkdownEditorContent', () => {
|
||||
return cy.getMarkdownEditor().selectAll().backspace({ times: 2 });
|
||||
});
|
||||
|
||||
Cypress.Commands.add('confirmRawEditorContent', expectedDomString => {
|
||||
cy.get('.cms-editor-raw').within(() => {
|
||||
cy.contains('span', expectedDomString);
|
||||
});
|
||||
});
|
||||
|
||||
function toPlainTree(domString: string) {
|
||||
return rehype()
|
||||
.use(removeSlateArtifacts)
|
||||
.data('settings', { fragment: true })
|
||||
.processSync(domString)
|
||||
.toString();
|
||||
}
|
||||
|
||||
function getActualBlockChildren(node: any) {
|
||||
if (node.tagName === 'span') {
|
||||
return node.children.flatMap(getActualBlockChildren);
|
||||
}
|
||||
if (node.children) {
|
||||
return {
|
||||
...node,
|
||||
children: node.children.flatMap(getActualBlockChildren),
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function removeSlateArtifacts() {
|
||||
return function transform(tree: any) {
|
||||
visit(tree, 'element', node => {
|
||||
// remove all element attributes
|
||||
delete node.properties;
|
||||
|
||||
// remove slate padding spans to simplify test cases
|
||||
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'].includes(node.tagName)) {
|
||||
node.children = node.children.flatMap(getActualBlockChildren);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getTextNode(el: Node, match?: string): Text | null {
|
||||
const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
|
||||
if (!match) {
|
||||
return walk.nextNode() as Text;
|
||||
}
|
||||
|
||||
let node: Text;
|
||||
while ((node = walk.nextNode() as Text)) {
|
||||
if (node.wholeText.includes(match)) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setBaseAndExtent(
|
||||
anchorNode: Node | null,
|
||||
anchorOffset: number,
|
||||
focusNode: Node | null,
|
||||
focusOffset: number,
|
||||
) {
|
||||
if (!anchorNode || !focusNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = anchorNode?.ownerDocument;
|
||||
document?.getSelection()?.removeAllRanges();
|
||||
document?.getSelection()?.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
}
|
27
cypress/support/e2e.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
import 'cypress-plugin-tab';
|
||||
import { addMatchImageSnapshotCommand } from '@simonsmith/cypress-image-snapshot/command';
|
||||
|
||||
addMatchImageSnapshotCommand({
|
||||
failureThreshold: 0.01,
|
||||
failureThresholdType: 'percent',
|
||||
customDiffConfig: { threshold: 0.09 },
|
||||
capture: 'viewport',
|
||||
});
|
||||
|
||||
Cypress.on('uncaught:exception', () => false);
|
||||
|
||||
import './commands';
|
5
cypress/utils/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
## Utilities for integration tests
|
||||
|
||||
Utils in this dir must be explicitly included in each spec file.
|
||||
|
||||
For routines to be executed on all tests, please use the `cypress/plugins.index.js` file instead: https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Plugin-files
|