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>
8
.github/workflows/build.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
packages/app/yarn.lock
|
packages/app/yarn.lock
|
||||||
packages/core/yarn.lock
|
packages/core/yarn.lock
|
||||||
packages/docs/yarn.lock
|
packages/docs/yarn.lock
|
||||||
node-version: 16
|
node-version: 18
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
@ -50,7 +50,7 @@ jobs:
|
|||||||
packages/app/yarn.lock
|
packages/app/yarn.lock
|
||||||
packages/core/yarn.lock
|
packages/core/yarn.lock
|
||||||
packages/docs/yarn.lock
|
packages/docs/yarn.lock
|
||||||
node-version: 16
|
node-version: 18
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
@ -75,7 +75,7 @@ jobs:
|
|||||||
packages/app/yarn.lock
|
packages/app/yarn.lock
|
||||||
packages/core/yarn.lock
|
packages/core/yarn.lock
|
||||||
packages/docs/yarn.lock
|
packages/docs/yarn.lock
|
||||||
node-version: 16
|
node-version: 18
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
@ -105,7 +105,7 @@ jobs:
|
|||||||
packages/app/yarn.lock
|
packages/app/yarn.lock
|
||||||
packages/core/yarn.lock
|
packages/core/yarn.lock
|
||||||
packages/docs/yarn.lock
|
packages/docs/yarn.lock
|
||||||
node-version: 16
|
node-version: 18
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: |
|
run: |
|
||||||
|
49
.github/workflows/cypress.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
name: Cypress Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: ['main', 'next']
|
||||||
|
pull_request:
|
||||||
|
branches: ['main', 'next']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cypress-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false # https://github.com/cypress-io/github-action/issues/48
|
||||||
|
matrix:
|
||||||
|
containers: [1, 2] # Uses 2 parallel instances
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: yarn
|
||||||
|
cache-dependency-path: |
|
||||||
|
yarn.lock
|
||||||
|
packages/app/yarn.lock
|
||||||
|
packages/core/yarn.lock
|
||||||
|
packages/docs/yarn.lock
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: |
|
||||||
|
yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run Cypress Tests
|
||||||
|
uses: cypress-io/github-action@v6
|
||||||
|
with:
|
||||||
|
start: yarn dev
|
||||||
|
wait-on: 'http://localhost:8080'
|
||||||
|
record: true # Records to Cypress Cloud
|
||||||
|
parallel: true # Runs test in parallel using settings above
|
||||||
|
env:
|
||||||
|
# For recording and parallelization to work you must set your CYPRESS_RECORD_KEY
|
||||||
|
# in GitHub repo → Settings → Secrets → Actions
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
# Creating a token https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token
|
||||||
|
GITHUB_TOKEN: ${{ secrets.CYPRESS_GITHUB_TOKEN }}
|
4
.gitignore
vendored
@ -2,3 +2,7 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/downloads
|
||||||
|
.env
|
||||||
|
.temp
|
||||||
|
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
bin/
|
||||||
|
public/
|
||||||
|
.cache/
|
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
19
cypress.config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
import setupNodeEvents from "./cypress/plugins";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
projectId: "wvw3x3",
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
chromeWebSecurity: false,
|
||||||
|
e2e: {
|
||||||
|
video: false,
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents,
|
||||||
|
baseUrl: "http://localhost:8080",
|
||||||
|
specPattern: "cypress/e2e/*.spec.ts",
|
||||||
|
},
|
||||||
|
});
|
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 |