mirror of
https://github.com/strapi/strapi.git
synced 2025-06-27 00:41:25 +00:00
Merge branch 'develop' into v5/main
This commit is contained in:
commit
e188aac623
1
.github/filters.yaml
vendored
1
.github/filters.yaml
vendored
@ -7,6 +7,7 @@ backend:
|
||||
- 'packages/{utils,generators,cli,providers}/**'
|
||||
- 'packages/core/*/{lib,bin,ee,src}/**'
|
||||
- 'tests/api/**'
|
||||
- 'packages/core/database/**'
|
||||
frontend:
|
||||
- '.github/actions/yarn-nm-install/*.yml'
|
||||
- '.github/workflows/**'
|
||||
|
18
.github/workflows/issues_handleLabel.yml
vendored
18
.github/workflows/issues_handleLabel.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
actions: 'add-assignees'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
assignees: 'derrickmehaffy, kasonde, bolg55, Eventyret'
|
||||
assignees: 'derrickmehaffy, kasonde, bolg55, Eventyret, dennism501, n-alonso'
|
||||
random-to: 1
|
||||
|
||||
# v3 Legacy Issues
|
||||
@ -183,25 +183,17 @@ jobs:
|
||||
# SLACK_USERNAME: Strapi-Alerts
|
||||
# SLACK_FOOTER: Triggered by GitHub Actions
|
||||
|
||||
# Auto assign issues to projects based on source
|
||||
- name: assign issues to Content squad project
|
||||
uses: actions/add-to-project@v0.4.1
|
||||
with:
|
||||
project-url: https://github.com/orgs/strapi/projects/11
|
||||
github-token: ${{ secrets.PROJECT_TRANSFER_TOKEN }}
|
||||
labeled: 'source: core:admin, source: core:content-manager, source: core:upload, source: plugin:i18n'
|
||||
label-operator: OR
|
||||
- name: assign issues to DevExp squad project
|
||||
uses: actions/add-to-project@v0.4.1
|
||||
with:
|
||||
project-url: https://github.com/orgs/strapi/projects/13
|
||||
github-token: ${{ secrets.PROJECT_TRANSFER_TOKEN }}
|
||||
labeled: 'source: core:content-type-builder, source: core:database, source: core:email, source: core:strapi, source: core:utils, source: plugin:graphql, source: plugin:users-permissions, source: typescript, source: core:data-transfer'
|
||||
labeled: 'source: cli, source: core:content-type-builder, source: core:core, source: core:data-transfer, source: core:database, source: core:strapi, source: core:utils, source: dependencies, source: docs, source: external, source: generators:app, source: generators, source: marketplace, source: plugin:graphql, source: plugin:users-permissions, source: tooling, source: typescript, source: utils:pack-up, source: utils:upgrade'
|
||||
label-operator: OR
|
||||
- name: assign issues to Expansions squad project
|
||||
- name: assign issues to Content squad project
|
||||
uses: actions/add-to-project@v0.4.1
|
||||
with:
|
||||
project-url: https://github.com/orgs/strapi/projects/4
|
||||
project-url: https://github.com/orgs/strapi/projects/23
|
||||
github-token: ${{ secrets.PROJECT_TRANSFER_TOKEN }}
|
||||
labeled: 'source: marketplace, source: plugin:sentry, source: plugin:documentation'
|
||||
labeled: 'source: core:admin, source: core:content-manager, source: core:content-releases, source: core:email, source: core:helper-plugin, source: core:permissions, source: core:review-workflows, source: core:upload, source: plugin:cloud, source: plugin:color-picker, source: plugin:documentation, source: , plugin:i18n, source: plugin:sentry, source: providers'
|
||||
label-operator: OR
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -100,18 +100,14 @@ package-lock.json
|
||||
test-apps
|
||||
coverage
|
||||
|
||||
############################
|
||||
# Documentation
|
||||
############################
|
||||
|
||||
dist
|
||||
|
||||
############################
|
||||
# Builds
|
||||
############################
|
||||
|
||||
packages/generators/app/files/public/
|
||||
schema.graphql
|
||||
dist
|
||||
.nx
|
||||
|
||||
############################
|
||||
# Example app
|
||||
|
@ -34,15 +34,27 @@ If you need to clean the test-apps folder because they are not working as expect
|
||||
To run only one domain, meaning a top-level directory in e2e/tests such as "admin" or "content-manager", use the `--domains` option.
|
||||
|
||||
```shell
|
||||
yarn test:e2e --domains admin
|
||||
yarn test:e2e --domain admin
|
||||
yarn test:e2e --domains=admin
|
||||
npm run test:e2e --domains=admin
|
||||
```
|
||||
|
||||
To run a specific file, you can pass arguments and options to playwright using `--` between the test:e2e options and the playwright options, such as:
|
||||
|
||||
```shell
|
||||
# to run just the login.spec.ts file in the admin domain
|
||||
yarn test:e2e --domains admin -- login.spec.ts
|
||||
yarn test:e2e --domains=admin -- login.spec.ts
|
||||
npm run test:e2e --domains=admin -- login.spec.ts
|
||||
```
|
||||
|
||||
Note that you must still include a domain, otherwise playwright will attempt to run every domain filtering by that filename, and any domains that do not contain that filename will fail with "no tests found"
|
||||
|
||||
### Running specific browsers
|
||||
|
||||
To run only a specific browser (to speed up test development, for example) you can pass `--project` to playwright with the value(s) `chromium`, `firefox`, or `webkit`
|
||||
|
||||
```shell
|
||||
yarn test:e2e --domains=admin -- login.spec.ts --project=chromium
|
||||
npm run test:e2e --domains=admin -- login.spec.ts --project=chromium
|
||||
```
|
||||
|
||||
### Concurrency / parallellization
|
||||
|
@ -134,6 +134,12 @@
|
||||
"type": "relation",
|
||||
"relation": "morphToMany"
|
||||
},
|
||||
"morph_one": {
|
||||
"type": "relation",
|
||||
"relation": "morphOne",
|
||||
"target": "api::tag.tag",
|
||||
"morphBy": "taggable"
|
||||
},
|
||||
"custom_field": {
|
||||
"type": "customField",
|
||||
"customField": "plugin::color-picker.color"
|
||||
|
@ -38,6 +38,19 @@
|
||||
"relation": "oneToOne",
|
||||
"target": "api::kitchensink.kitchensink",
|
||||
"mappedBy": "one_to_one_tag"
|
||||
},
|
||||
"taggable": {
|
||||
"type": "relation",
|
||||
"relation": "morphToOne",
|
||||
"morphColumn": {
|
||||
"typeColumn": {
|
||||
"name": "taggable_type"
|
||||
},
|
||||
"idColumn": {
|
||||
"name": "taggable_id",
|
||||
"referencedColumn": "id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
|
||||
import { mockData } from '@tests/mockData';
|
||||
import { render, waitFor, server } from '@tests/utils';
|
||||
import { render, waitFor, server, screen } from '@tests/utils';
|
||||
import { rest } from 'msw';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useRBAC } from '../../../../../hooks/useRBAC';
|
||||
import { ListPage } from '../ListPage';
|
||||
|
||||
jest.mock('../../../../../hooks/useRBAC');
|
||||
|
||||
const LocationDisplay = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return <span data-testId="location">{location.pathname}</span>;
|
||||
};
|
||||
|
||||
describe('Webhooks | ListPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@ -28,12 +34,6 @@ describe('Webhooks | ListPage', () => {
|
||||
});
|
||||
|
||||
it('should show a loader when permissions are loading', async () => {
|
||||
// @ts-expect-error – mocking for test
|
||||
useRBAC.mockImplementationOnce(() => ({
|
||||
isLoading: true,
|
||||
allowedActions: { canUpdate: true, canCreate: true, canDelete: true },
|
||||
}));
|
||||
|
||||
const { queryByText, getByText } = render(<ListPage />);
|
||||
|
||||
expect(getByText('Loading content.')).toBeInTheDocument();
|
||||
@ -139,4 +139,34 @@ describe('Webhooks | ListPage', () => {
|
||||
expect(enableSwitches[0]).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to create a new webhook on empty state screen by clicking on the button', async () => {
|
||||
server.use(
|
||||
rest.get('/admin/webhooks', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: [],
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const { getAllByRole, findByText, user } = render(<ListPage />, {
|
||||
renderOptions: {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<LocationDisplay />
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await findByText('No webhooks found');
|
||||
expect(screen.getByTestId('location')).not.toHaveTextContent('/create');
|
||||
await user.click(getAllByRole('link', { name: 'Create new webhook' })[1]);
|
||||
await waitFor(() => expect(screen.getByTestId('location')).toHaveTextContent('/create'));
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { StrapiApp, StrapiAppConstructorArgs } from './StrapiApp';
|
||||
import { getFetchClient } from './utils/getFetchClient';
|
||||
import { createAbsoluteUrl } from './utils/urls';
|
||||
|
||||
import type { Modules } from '@strapi/types';
|
||||
|
||||
@ -30,7 +31,7 @@ const renderAdmin = async (
|
||||
*
|
||||
* To ensure that the backendURL is always set, we use the window.location.origin as a fallback.
|
||||
*/
|
||||
backendURL: process.env.STRAPI_ADMIN_BACKEND_URL || window.location.origin,
|
||||
backendURL: createAbsoluteUrl(process.env.STRAPI_ADMIN_BACKEND_URL),
|
||||
isEE: false,
|
||||
telemetryDisabled: process.env.STRAPI_TELEMETRY_DISABLED === 'true',
|
||||
future: {
|
||||
|
25
packages/core/admin/admin/src/utils/tests/urls.test.ts
Normal file
25
packages/core/admin/admin/src/utils/tests/urls.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { createAbsoluteUrl } from '../urls';
|
||||
|
||||
describe('urls', () => {
|
||||
describe('createAbsoluteUrl', () => {
|
||||
it('should return the url if it is an absolute URL', () => {
|
||||
expect(createAbsoluteUrl('https://example.com')).toMatchInlineSnapshot(
|
||||
`"https://example.com"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the window.location.origin if the url is not provided', () => {
|
||||
expect(createAbsoluteUrl()).toMatchInlineSnapshot(`"http://localhost:1337"`);
|
||||
});
|
||||
|
||||
it('should return the window.location.origin prefixed to the provided url if the url is relative', () => {
|
||||
expect(createAbsoluteUrl('/example')).toMatchInlineSnapshot(
|
||||
`"http://localhost:1337/example"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle protocol relative URLs', () => {
|
||||
expect(createAbsoluteUrl('//example.com')).toMatchInlineSnapshot(`"http://example.com/"`);
|
||||
});
|
||||
});
|
||||
});
|
@ -2,4 +2,24 @@ const prefixFileUrlWithBackendUrl = (fileURL?: string): string | undefined => {
|
||||
return !!fileURL && fileURL.startsWith('/') ? `${window.strapi.backendURL}${fileURL}` : fileURL;
|
||||
};
|
||||
|
||||
export { prefixFileUrlWithBackendUrl };
|
||||
/**
|
||||
* @description Creates an absolute URL, if there is no URL or it
|
||||
* is relative, we use the `window.location.origin` as a fallback.
|
||||
* IF it's an absolute URL, we return it as is.
|
||||
*/
|
||||
const createAbsoluteUrl = (url?: string): string => {
|
||||
if (!url) {
|
||||
return window.location.origin;
|
||||
}
|
||||
if (url.startsWith('/')) {
|
||||
/**
|
||||
* This will also manage protocol relative URLs which is fine because
|
||||
* as we can see from the test, we still get the expected result.
|
||||
*/
|
||||
return new URL(url, window.location.origin).toString();
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
export { createAbsoluteUrl, prefixFileUrlWithBackendUrl };
|
||||
|
@ -23,9 +23,14 @@ export const transformUpgradeHeader = (header = '') => {
|
||||
|
||||
let timeouts: Record<string, number> | undefined;
|
||||
|
||||
const hasHttpServer = () => {
|
||||
// during server restarts, strapi may not have ever been defined at all, so we have to check it first
|
||||
return typeof strapi !== 'undefined' && !!strapi?.server?.httpServer;
|
||||
};
|
||||
|
||||
// temporarily disable server timeouts while transfer is running
|
||||
const disableTimeouts = () => {
|
||||
if (!strapi?.server?.httpServer) {
|
||||
if (!hasHttpServer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -45,7 +50,7 @@ const disableTimeouts = () => {
|
||||
strapi.log.info('[Data transfer] Disabling http timeouts');
|
||||
};
|
||||
const resetTimeouts = () => {
|
||||
if (!strapi?.server?.httpServer || !timeouts) {
|
||||
if (!hasHttpServer() || !timeouts) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -220,8 +220,7 @@ const createMorphToOne = (attributeName: string, attribute: Relation.MorphToOne)
|
||||
|
||||
Object.assign(attribute, {
|
||||
owner: true,
|
||||
morphColumn: {
|
||||
// TODO: add referenced column
|
||||
morphColumn: attribute.morphColumn ?? {
|
||||
typeColumn: {
|
||||
name: typeColumnName,
|
||||
},
|
||||
@ -231,8 +230,6 @@ const createMorphToOne = (attributeName: string, attribute: Relation.MorphToOne)
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: implement bidirectional
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -54,7 +54,7 @@ const createPivotJoin = (
|
||||
};
|
||||
|
||||
const createJoin = (ctx: Ctx, { alias, refAlias, attributeName, attribute }: JoinOptions) => {
|
||||
const { db, qb } = ctx;
|
||||
const { db, qb, uid } = ctx;
|
||||
|
||||
if (attribute.type !== 'relation') {
|
||||
throw new Error(`Cannot join on non relational field ${attributeName}`);
|
||||
@ -62,6 +62,61 @@ const createJoin = (ctx: Ctx, { alias, refAlias, attributeName, attribute }: Joi
|
||||
|
||||
const targetMeta = db.metadata.get(attribute.target);
|
||||
|
||||
if (['morphOne', 'morphMany'].includes(attribute.relation)) {
|
||||
const targetAttribute = targetMeta.attributes[attribute.morphBy];
|
||||
|
||||
// @ts-expect-error - morphBy is not defined on the attribute
|
||||
const { joinTable, morphColumn } = targetAttribute;
|
||||
|
||||
if (morphColumn) {
|
||||
const subAlias = refAlias || qb.getAlias();
|
||||
|
||||
qb.join({
|
||||
alias: subAlias,
|
||||
referencedTable: targetMeta.tableName,
|
||||
referencedColumn: morphColumn.idColumn.name,
|
||||
rootColumn: morphColumn.idColumn.referencedColumn,
|
||||
rootTable: alias,
|
||||
on: {
|
||||
[morphColumn.typeColumn.name]: uid,
|
||||
...morphColumn.on,
|
||||
},
|
||||
});
|
||||
|
||||
return subAlias;
|
||||
}
|
||||
|
||||
if (joinTable) {
|
||||
const joinAlias = qb.getAlias();
|
||||
|
||||
qb.join({
|
||||
alias: joinAlias,
|
||||
referencedTable: joinTable.name,
|
||||
referencedColumn: joinTable.morphColumn.idColumn.name,
|
||||
rootColumn: joinTable.morphColumn.idColumn.referencedColumn,
|
||||
rootTable: alias,
|
||||
on: {
|
||||
[joinTable.morphColumn.typeColumn.name]: uid,
|
||||
field: attributeName,
|
||||
},
|
||||
});
|
||||
|
||||
const subAlias = refAlias || qb.getAlias();
|
||||
|
||||
qb.join({
|
||||
alias: subAlias,
|
||||
referencedTable: targetMeta.tableName,
|
||||
referencedColumn: joinTable.joinColumn.referencedColumn,
|
||||
rootColumn: joinTable.joinColumn.name,
|
||||
rootTable: joinAlias,
|
||||
});
|
||||
|
||||
return subAlias;
|
||||
}
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
const { joinColumn } = attribute;
|
||||
|
||||
if (joinColumn) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { isArray, castArray, keys, isPlainObject } from 'lodash/fp';
|
||||
import { isArray, castArray, isPlainObject } from 'lodash/fp';
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import { isOperatorOfType } from '@strapi/utils';
|
||||
import { isOperator, isOperatorOfType } from '@strapi/utils';
|
||||
import * as types from '../../utils/types';
|
||||
import { createField } from '../../fields';
|
||||
import { createJoin } from './join';
|
||||
@ -12,6 +12,8 @@ import { isKnexQuery } from '../../utils/knex';
|
||||
import type { Ctx } from '../types';
|
||||
import type { Attribute } from '../../types';
|
||||
|
||||
type WhereCtx = Ctx & { alias?: string; isGroupRoot?: boolean };
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => isPlainObject(value);
|
||||
|
||||
const castValue = (value: unknown, attribute: Attribute | null) => {
|
||||
@ -72,7 +74,34 @@ const processNested = (where: unknown, ctx: WhereCtx) => {
|
||||
return processWhere(where, ctx);
|
||||
};
|
||||
|
||||
type WhereCtx = Ctx & { alias?: string };
|
||||
const processRelationWhere = (where: unknown, ctx: WhereCtx) => {
|
||||
const { qb, alias } = ctx;
|
||||
|
||||
const idAlias = qb.aliasColumn('id', alias);
|
||||
if (!isRecord(where)) {
|
||||
return { [idAlias]: where };
|
||||
}
|
||||
|
||||
const keys = Object.keys(where);
|
||||
const operatorKeys = keys.filter((key) => isOperator(key));
|
||||
|
||||
if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) {
|
||||
throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`);
|
||||
}
|
||||
|
||||
if (operatorKeys.length > 1) {
|
||||
throw new Error(
|
||||
`Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`
|
||||
);
|
||||
}
|
||||
|
||||
if (operatorKeys.length === 1) {
|
||||
const operator = operatorKeys[0];
|
||||
return { [idAlias]: { [operator]: processNested(where[operator], ctx) } };
|
||||
}
|
||||
|
||||
return processWhere(where, ctx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process where parameter
|
||||
@ -100,8 +129,12 @@ function processWhere(
|
||||
for (const key of Object.keys(where)) {
|
||||
const value = where[key];
|
||||
|
||||
// if operator $and $or then loop over them
|
||||
if (isOperatorOfType('group', key) && Array.isArray(value)) {
|
||||
// if operator $and $or -> process recursively
|
||||
if (isOperatorOfType('group', key)) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`Operator ${key} must be an array`);
|
||||
}
|
||||
|
||||
filters[key] = value.map((sub) => processNested(sub, ctx));
|
||||
continue;
|
||||
}
|
||||
@ -132,17 +165,13 @@ function processWhere(
|
||||
attribute,
|
||||
});
|
||||
|
||||
let nestedWhere = processNested(value, {
|
||||
const nestedWhere = processRelationWhere(value, {
|
||||
db,
|
||||
qb,
|
||||
alias: subAlias,
|
||||
uid: attribute.target,
|
||||
});
|
||||
|
||||
if (!isRecord(nestedWhere) || isOperatorOfType('where', keys(nestedWhere)[0])) {
|
||||
nestedWhere = { [qb.aliasColumn('id', subAlias)]: nestedWhere };
|
||||
}
|
||||
|
||||
// TODO: use a better merge logic (push to $and when collisions)
|
||||
Object.assign(filters, nestedWhere);
|
||||
|
||||
|
@ -64,11 +64,6 @@ const createConfig = ({ port, testDir, appDir }) => ({
|
||||
|
||||
/* Default time each action such as `click()` can take to 20s */
|
||||
actionTimeout: getEnvNum(process.env.PLAYWRIGHT_ACTION_TIMEOUT, 20 * 1000),
|
||||
|
||||
/* Collect trace when a test failed on the CI. See https://playwright.dev/docs/trace-viewer
|
||||
Until https://github.com/strapi/strapi/issues/18196 is fixed we can't enable this locally,
|
||||
because the Strapi server restarts every time a new file (trace) is created.
|
||||
*/
|
||||
trace: 'retain-on-failure',
|
||||
video: getEnvBool(process.env.PLAYWRIGHT_VIDEO, false)
|
||||
? {
|
||||
@ -105,8 +100,10 @@ const createConfig = ({ port, testDir, appDir }) => ({
|
||||
},
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: getEnvString(process.env.PLAYWRIGHT_OUTPUT_DIR, '../test-results/'), // in the test-apps/e2e dir, to avoid writing files to the running Strapi project dir
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc.
|
||||
* Must be outside the project itself or develop mode will restart when files are written
|
||||
* */
|
||||
outputDir: getEnvString(process.env.PLAYWRIGHT_OUTPUT_DIR, '../test-results/'),
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
|
@ -202,4 +202,201 @@ describe('Upload plugin', () => {
|
||||
.delete({ where: { id: dogEntity.profilePicture.id } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering data based on media attributes', () => {
|
||||
let uploadRes;
|
||||
let dogRes;
|
||||
|
||||
beforeAll(async () => {
|
||||
await Promise.all(
|
||||
data.dogs.map((dog) => {
|
||||
return strapi.entityService.delete('api::dog.dog', dog.data.id);
|
||||
})
|
||||
);
|
||||
|
||||
uploadRes = await rq({
|
||||
method: 'POST',
|
||||
url: '/upload',
|
||||
formData: {
|
||||
files: fs.createReadStream(path.join(__dirname, '../utils/rec.jpg')),
|
||||
fileInfo: JSON.stringify({
|
||||
alternativeText: 'rec',
|
||||
caption: 'my caption',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
dogRes = await rq({
|
||||
method: 'POST',
|
||||
url: '/dogs',
|
||||
body: {
|
||||
data: {
|
||||
profilePicture: {
|
||||
id: uploadRes.body[0].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await rq({
|
||||
method: 'DELETE',
|
||||
url: `/dogs/${dogRes.body.data.id}`,
|
||||
});
|
||||
|
||||
await rq({
|
||||
method: 'DELETE',
|
||||
url: `/upload/files/${uploadRes.body[0].id}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('can filter on notNull', async () => {
|
||||
let res;
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: { $notNull: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(1);
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: { $notNull: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
test('can filter on null', async () => {
|
||||
let res;
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: { $null: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(0);
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: { $null: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(1);
|
||||
});
|
||||
|
||||
test('can filter on id', async () => {
|
||||
let res;
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: uploadRes.body[0].id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(1);
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: 999999999,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
test('can filter media attribute', async () => {
|
||||
let res;
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: { ext: '.jpg' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(1);
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: { ext: '.pdf' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(0);
|
||||
});
|
||||
|
||||
test('can filter media attribute with operators', async () => {
|
||||
let res;
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: {
|
||||
caption: {
|
||||
$contains: 'my',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(1);
|
||||
|
||||
res = await rq({
|
||||
method: 'GET',
|
||||
url: '/dogs',
|
||||
qs: {
|
||||
filters: {
|
||||
profilePicture: {
|
||||
caption: {
|
||||
$contains: 'not',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.body.data.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,95 +1,4 @@
|
||||
const {
|
||||
file: {
|
||||
providers: { createLocalFileDestinationProvider },
|
||||
},
|
||||
strapi: {
|
||||
providers: { createLocalStrapiSourceProvider },
|
||||
},
|
||||
engine: { createTransferEngine },
|
||||
} = require('@strapi/data-transfer');
|
||||
const { createStrapi, compileStrapi } = require('@strapi/strapi');
|
||||
const { ALLOWED_CONTENT_TYPES } = require('../constants');
|
||||
|
||||
/**
|
||||
* Export the data from a strapi project.
|
||||
* This script should be run as `node <path-to>/dts-export.js [exportFilePath]` from the
|
||||
* root directory of a strapi project e.g. `/examples/kitchensink`. Remember to import
|
||||
* the `with-admin` tar file into the project first because the tests rely on the data.
|
||||
*/
|
||||
const exportData = async () => {
|
||||
let args = process.argv.slice(2);
|
||||
|
||||
if (args.length !== 1) {
|
||||
console.error('Please provide the export file name as a parameter.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const strapi = await createStrapiInstance();
|
||||
|
||||
const source = createSourceProvider(strapi);
|
||||
const destination = createDestinationProvider(args[0]);
|
||||
|
||||
const engine = createTransferEngine(source, destination, {
|
||||
versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
|
||||
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
|
||||
only: ['content', 'files'],
|
||||
transforms: {
|
||||
links: [
|
||||
{
|
||||
filter(link) {
|
||||
return (
|
||||
ALLOWED_CONTENT_TYPES.includes(link.left.type) &&
|
||||
ALLOWED_CONTENT_TYPES.includes(link.right.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
filter(entity) {
|
||||
return ALLOWED_CONTENT_TYPES.includes(entity.type);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
engine.diagnostics.onDiagnostic(console.log);
|
||||
|
||||
try {
|
||||
const results = await engine.transfer();
|
||||
|
||||
console.log(JSON.stringify(results.engine, null, 2));
|
||||
} catch {
|
||||
console.error('Export process failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const createSourceProvider = (strapi) =>
|
||||
createLocalStrapiSourceProvider({
|
||||
async getStrapi() {
|
||||
return strapi;
|
||||
},
|
||||
});
|
||||
|
||||
const createDestinationProvider = (filePath) =>
|
||||
createLocalFileDestinationProvider({
|
||||
file: { path: filePath },
|
||||
encryption: { enabled: false },
|
||||
compression: { enabled: false },
|
||||
});
|
||||
|
||||
const createStrapiInstance = async (logLevel = 'error') => {
|
||||
const appContext = await compileStrapi();
|
||||
const app = createStrapi(appContext);
|
||||
|
||||
app.log.level = logLevel;
|
||||
const loadedApp = await app.load();
|
||||
|
||||
return loadedApp;
|
||||
};
|
||||
const { exportData } = require('../utils/dts-export');
|
||||
|
||||
// TODO: make an actual yargs command and pass common options to exportData so it's easier to build the test data
|
||||
exportData();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { toggleRateLimiting } from '../../scripts/endpoints';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
import { toggleRateLimiting } from '../../utils/rate-limit';
|
||||
import { ADMIN_EMAIL_ADDRESS, ADMIN_PASSWORD } from '../../constants';
|
||||
import { login } from '../../utils/login';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
import { login } from '../../utils/login';
|
||||
|
||||
test.describe('Log Out', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
import { ADMIN_EMAIL_ADDRESS, ADMIN_PASSWORD } from '../../constants';
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from '../../utils/login';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
import { findAndClose } from '../../utils/shared';
|
||||
|
||||
test.describe('Edit View', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from '../../utils/login';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
|
||||
test.describe('List View', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { describeOnCondition } from '../../utils/shared';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
import { login } from '../../utils/login';
|
||||
|
||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { describeOnCondition, navToHeader } from '../../utils/shared';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
import { login } from '../../utils/login';
|
||||
|
||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from '../../../utils/login';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../../utils/dts-import';
|
||||
import { waitForRestart } from '../../../utils/restart';
|
||||
import { resetFiles } from '../../../utils/file-reset';
|
||||
|
||||
test.describe('Create collection type', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await resetFiles();
|
||||
await resetDatabaseAndImportDataFromPath('with-admin.tar');
|
||||
await page.goto('/admin');
|
||||
await login({ page });
|
||||
|
||||
await page.getByRole('link', { name: 'Content-Type Builder' }).click();
|
||||
|
||||
// close the tutorial modal if it's visible
|
||||
const modal = page.getByRole('button', { name: 'Close' });
|
||||
if (modal.isVisible()) {
|
||||
await modal.click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: each test should have a beforeAll that does this, maybe combine all the setup into one util to simplify it
|
||||
// to keep other suites that don't modify files from needing to reset files, clean up after ourselves at the end
|
||||
test.afterAll(async () => {
|
||||
await resetFiles();
|
||||
});
|
||||
|
||||
test('Can create a collection type', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Create new collection type' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Create a collection type' })).toBeVisible();
|
||||
|
||||
const displayName = page.getByLabel('Display name');
|
||||
await displayName.fill('Secret Document');
|
||||
|
||||
const singularId = page.getByLabel('API ID (Singular)');
|
||||
await expect(singularId).toHaveValue('secret-document');
|
||||
|
||||
const pluralId = page.getByLabel('API ID (Plural)');
|
||||
await expect(pluralId).toHaveValue('secret-documents');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByText('Select a field for your collection type')).toBeVisible();
|
||||
await page.getByText('Small or long text').click();
|
||||
await page.getByLabel('Name', { exact: true }).fill('myattribute');
|
||||
await page.getByRole('button', { name: 'Finish' }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await waitForRestart(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Secret Document' })).toBeVisible();
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from '../../utils/login';
|
||||
import { navToHeader } from '../../utils/shared';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../scripts/dts-import';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
|
||||
test.describe('Edit View CTB', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
26
tests/e2e/tests/content-type-builder/tutorial.spec.ts
Normal file
26
tests/e2e/tests/content-type-builder/tutorial.spec.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from '../../utils/login';
|
||||
import { resetDatabaseAndImportDataFromPath } from '../../utils/dts-import';
|
||||
|
||||
test.describe('Tutorial', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await resetDatabaseAndImportDataFromPath('with-admin.tar');
|
||||
await page.goto('/admin');
|
||||
await login({ page });
|
||||
});
|
||||
|
||||
test('Shows tutorial on first content type', async ({ page }) => {
|
||||
await page.getByRole('link', { name: 'Content-type Builder' }).click();
|
||||
|
||||
const modalHeader = page.getByRole('heading', { name: '🧠 Create a first Collection' });
|
||||
expect(modalHeader).toBeVisible();
|
||||
await modalHeader.click();
|
||||
|
||||
const closeButton = page.getByRole('button', { name: 'Close' });
|
||||
expect(closeButton).toBeVisible();
|
||||
await closeButton.click();
|
||||
|
||||
await expect(closeButton).not.toBeVisible();
|
||||
await expect(modalHeader).not.toBeVisible();
|
||||
});
|
||||
});
|
93
tests/e2e/utils/dts-export.js
Normal file
93
tests/e2e/utils/dts-export.js
Normal file
@ -0,0 +1,93 @@
|
||||
const {
|
||||
file: {
|
||||
providers: { createLocalFileDestinationProvider },
|
||||
},
|
||||
strapi: {
|
||||
providers: { createLocalStrapiSourceProvider },
|
||||
},
|
||||
engine: { createTransferEngine },
|
||||
} = require('@strapi/data-transfer');
|
||||
const strapiFactory = require('@strapi/strapi');
|
||||
const { ALLOWED_CONTENT_TYPES } = require('../constants');
|
||||
|
||||
/**
|
||||
* Export the data from a strapi project.
|
||||
* This script should be run as `node <path-to>/dts-export.js [exportFilePath]` from the
|
||||
* root directory of a strapi project e.g. `/examples/kitchensink`. Remember to import
|
||||
* the `with-admin` tar file into the project first because the tests rely on the data.
|
||||
*/
|
||||
export const exportData = async () => {
|
||||
let args = process.argv.slice(2);
|
||||
|
||||
if (args.length !== 1) {
|
||||
console.error('Please provide the export file name as a parameter.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const strapi = await createStrapiInstance();
|
||||
|
||||
const source = createSourceProvider(strapi);
|
||||
const destination = createDestinationProvider(args[0]);
|
||||
|
||||
const engine = createTransferEngine(source, destination, {
|
||||
versionStrategy: 'ignore', // for an export to file, versionStrategy will always be skipped
|
||||
schemaStrategy: 'ignore', // for an export to file, schemaStrategy will always be skipped
|
||||
only: ['content', 'files'],
|
||||
transforms: {
|
||||
links: [
|
||||
{
|
||||
filter(link) {
|
||||
return (
|
||||
ALLOWED_CONTENT_TYPES.includes(link.left.type) &&
|
||||
ALLOWED_CONTENT_TYPES.includes(link.right.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
entities: [
|
||||
{
|
||||
filter(entity) {
|
||||
return ALLOWED_CONTENT_TYPES.includes(entity.type);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
engine.diagnostics.onDiagnostic(console.log);
|
||||
|
||||
try {
|
||||
const results = await engine.transfer();
|
||||
|
||||
console.log(JSON.stringify(results.engine, null, 2));
|
||||
} catch {
|
||||
console.error('Export process failed.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
const createSourceProvider = (strapi) =>
|
||||
createLocalStrapiSourceProvider({
|
||||
async getStrapi() {
|
||||
return strapi;
|
||||
},
|
||||
});
|
||||
|
||||
const createDestinationProvider = (filePath) =>
|
||||
createLocalFileDestinationProvider({
|
||||
file: { path: filePath },
|
||||
encryption: { enabled: false },
|
||||
compression: { enabled: false },
|
||||
});
|
||||
|
||||
const createStrapiInstance = async (logLevel = 'error') => {
|
||||
const appContext = await strapiFactory.compile();
|
||||
const app = strapiFactory(appContext);
|
||||
|
||||
app.log.level = logLevel;
|
||||
const loadedApp = await app.load();
|
||||
|
||||
return loadedApp;
|
||||
};
|
54
tests/e2e/utils/file-reset.js
Normal file
54
tests/e2e/utils/file-reset.js
Normal file
@ -0,0 +1,54 @@
|
||||
import execa from 'execa';
|
||||
|
||||
const gitUser = ['-c', 'user.name=Strapi CLI', '-c', 'user.email=test@strapi.io'];
|
||||
|
||||
function delay(seconds) {
|
||||
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
||||
}
|
||||
|
||||
export const pollHealthCheck = async (interval = 1000, timeout = 30000) => {
|
||||
const url = `http://127.0.0.1:${process.env.PORT ?? 1337}/_health`;
|
||||
console.log(`Starting to poll: ${url}`);
|
||||
|
||||
let elapsed = 0;
|
||||
|
||||
while (elapsed < timeout) {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
if (response.ok) {
|
||||
console.log('The service is up and running!');
|
||||
return; // Exit if the service is up
|
||||
}
|
||||
// If the response is not okay, throw an error to catch it below
|
||||
throw new Error('Service not ready');
|
||||
} catch (error) {
|
||||
console.log('Waiting for the service to come up...');
|
||||
// Wait for the specified interval before trying again
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
elapsed += interval; // Update the elapsed time
|
||||
}
|
||||
}
|
||||
|
||||
// If we've exited the loop because of the timeout
|
||||
console.error('Timeout reached, service did not become available in time.');
|
||||
};
|
||||
|
||||
export const resetFiles = async () => {
|
||||
if (process.env.TEST_APP_PATH) {
|
||||
console.log('Restoring filesystem');
|
||||
await execa('git', [...gitUser, 'reset', '--hard'], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.env.TEST_APP_PATH,
|
||||
});
|
||||
const dryRun = await execa('git', [...gitUser, 'clean', '-fd'], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.env.TEST_APP_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
// wait for server to restart after modifying files
|
||||
console.log('Waiting for Strapi to restart...');
|
||||
// TODO: this is both a waste of time and flaky. We need to find a way to access playwright server output and watch for the "up" log to appear
|
||||
await delay(3); // give it time to detect file changes and begin its restart
|
||||
await pollHealthCheck(); // give it time to come back up
|
||||
};
|
6
tests/e2e/utils/rate-limit.js
Normal file
6
tests/e2e/utils/rate-limit.js
Normal file
@ -0,0 +1,6 @@
|
||||
export async function toggleRateLimiting(page, enabled = true) {
|
||||
await page.request.fetch('/api/config/ratelimit/enable', {
|
||||
method: 'POST',
|
||||
data: { value: enabled },
|
||||
});
|
||||
}
|
50
tests/e2e/utils/restart.ts
Normal file
50
tests/e2e/utils/restart.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
// Function to check modal visibility
|
||||
const isModalVisible = async (page: Page) => {
|
||||
return page.isVisible('text="Waiting for restart..."');
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for a restart modal to appear, but instead of failing if it doesn't, attempt to
|
||||
* refresh the page and see if it comes back up
|
||||
*/
|
||||
export const waitForRestart = async (page, timeout = 60000) => {
|
||||
const initialWaitForModal = 5000; // Time to wait for the modal to initially appear
|
||||
let elapsedTime = 0;
|
||||
const checkInterval = 1000; // Check every 1 second
|
||||
const reloadTimeout = 15000; // 15 seconds before trying to reload
|
||||
|
||||
// Initially wait for the modal to become visible
|
||||
try {
|
||||
await page.waitForSelector('text="Waiting for restart..."', {
|
||||
state: 'visible',
|
||||
timeout: initialWaitForModal,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('The modal did not become visible within the initial wait period.');
|
||||
throw error; // Or handle this scenario as appropriate
|
||||
}
|
||||
|
||||
// Now wait until the modal is not visible or until the reloadTimeout
|
||||
let modalVisible = await isModalVisible(page);
|
||||
while (modalVisible && elapsedTime < reloadTimeout) {
|
||||
await new Promise((r) => setTimeout(r, checkInterval));
|
||||
elapsedTime += checkInterval;
|
||||
modalVisible = await isModalVisible(page);
|
||||
}
|
||||
|
||||
// If modal is still visible after reloadTimeout, reload the page and wait again
|
||||
if (modalVisible) {
|
||||
console.log("Restart overlay didn't disappear after 15 seconds. Reloading page...");
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
// Optionally, wait again for the modal to disappear after reloading
|
||||
}
|
||||
|
||||
// Final check to ensure the modal has disappeared
|
||||
if (await isModalVisible(page)) {
|
||||
throw new Error('Restart overlay did not disappear after waiting and reloading.');
|
||||
}
|
||||
|
||||
console.log('Restart overlay has disappeared, proceeding with the test.');
|
||||
};
|
@ -59,8 +59,8 @@ const setupTestEnvironment = async (generatedAppPath) => {
|
||||
yargs
|
||||
.parserConfiguration({
|
||||
/**
|
||||
* This lets us pass any other arguments to playwright
|
||||
* e.g. the name of a specific test or the project we want to run
|
||||
* When unknown options is false, using -- to separate playwright args from test:e2e args works
|
||||
* When it is true, the script gets confused about additional arguments, with or without using -- to separate commands
|
||||
*/
|
||||
'unknown-options-as-args': false,
|
||||
})
|
||||
@ -222,6 +222,28 @@ module.exports = config
|
||||
|
||||
await fs.writeFile(pathToPlaywrightConfig, configFileTemplate);
|
||||
|
||||
// Store the filesystem state with git so it can be reset between tests
|
||||
// TODO: if we have a large test test suite, it might be worth it to run a `strapi start` and then shutdown here to generate documentation and types only once and save unneccessary server restarts from those files being cleared every time
|
||||
console.log('Initializing git');
|
||||
|
||||
const gitUser = ['-c', 'user.name=Strapi CLI', '-c', 'user.email=test@strapi.io'];
|
||||
|
||||
await execa('git', [...gitUser, 'init'], {
|
||||
stdio: 'inherit',
|
||||
cwd: testAppPath,
|
||||
});
|
||||
|
||||
// we need to use -A to track even hidden files like .env; remember we're only using git as a file state manager
|
||||
await execa('git', [...gitUser, 'add', '-A', '.'], {
|
||||
stdio: 'inherit',
|
||||
cwd: testAppPath,
|
||||
});
|
||||
|
||||
await execa('git', [...gitUser, 'commit', '-m', 'initial commit'], {
|
||||
stdio: 'inherit',
|
||||
cwd: testAppPath,
|
||||
});
|
||||
|
||||
console.log(`Running ${chalk.blue(domain)} e2e tests`);
|
||||
|
||||
await execa(
|
||||
@ -233,6 +255,7 @@ module.exports = config
|
||||
env: {
|
||||
PORT: port,
|
||||
HOST: '127.0.0.1',
|
||||
TEST_APP_PATH: testAppPath,
|
||||
STRAPI_DISABLE_EE: !process.env.STRAPI_LICENSE,
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user