@appsemble/e2e 0.34.21 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/fixtures/expect/training/index.js +4 -1
- package/fixtures/test/app/index.d.ts +24 -2
- package/fixtures/test/app/index.js +40 -1
- package/fixtures/test/app-collection/index.d.ts +14 -7
- package/fixtures/test/app-collection/index.js +46 -32
- package/fixtures/test/index.d.ts +1 -1
- package/fixtures/test/index.js +2 -1
- package/fixtures/test/live-app/index.d.ts +4 -2
- package/fixtures/test/live-app/index.js +11 -5
- package/fixtures/test/organization/index.d.ts +26 -0
- package/fixtures/test/organization/index.js +35 -0
- package/fixtures/test/studio/index.d.ts +32 -0
- package/fixtures/test/studio/index.js +26 -1
- package/fixtures/test/test-utils/index.d.ts +5 -1
- package/fixtures/test/test-utils/index.js +15 -1
- package/package.json +4 -4
- package/playwright.config.js +21 -1
- package/tests/demo-apps/empty.spec.js +26 -0
- package/tests/{template-apps → demo-apps}/holidays.spec.js +13 -1
- package/tests/{template-apps → demo-apps}/notes.spec.js +12 -3
- package/tests/{template-apps → demo-apps}/person.spec.js +15 -1
- package/tests/static-ui/apps.spec.js +37 -0
- package/tests/static-ui/blocks.spec.js +35 -0
- package/tests/static-ui/individual-app.spec.js +69 -0
- package/tests/static-ui/organizations.spec.js +41 -0
- package/tests/static-ui/training.spec.js +39 -0
- package/tests/studio/app-collections/app-collections.spec.js +127 -103
- package/tests/studio/individual-app.spec.d.ts +1 -0
- package/tests/studio/individual-app.spec.js +51 -0
- package/tests/studio/organizations.spec.js +31 -24
- package/tests/studio/payments/subscription-flow.spec.d.ts +1 -0
- package/tests/studio/payments/subscription-flow.spec.js +68 -0
- package/tests/studio/trainings.spec.d.ts +1 -0
- package/tests/studio/trainings.spec.js +105 -0
- package/tests/studio/apps/app.spec.js +0 -79
- package/tests/studio/apps.spec.js +0 -30
- package/tests/studio/blocks.spec.js +0 -20
- package/tests/studio/trainings/constants.d.ts +0 -4
- package/tests/studio/trainings/constants.js +0 -5
- package/tests/studio/trainings/logged-in.spec.js +0 -93
- package/tests/studio/trainings/logged-out.spec.js +0 -42
- package/tests/template-apps/empty.spec.js +0 -14
- /package/tests/{template-apps → demo-apps}/empty.spec.d.ts +0 -0
- /package/tests/{template-apps → demo-apps}/holidays.spec.d.ts +0 -0
- /package/tests/{template-apps → demo-apps}/notes.spec.d.ts +0 -0
- /package/tests/{template-apps → demo-apps}/person.spec.d.ts +0 -0
- /package/tests/{studio → static-ui}/apps.spec.d.ts +0 -0
- /package/tests/{studio → static-ui}/blocks.spec.d.ts +0 -0
- /package/tests/{studio → static-ui}/docs.spec.d.ts +0 -0
- /package/tests/{studio → static-ui}/docs.spec.js +0 -0
- /package/tests/{studio/apps/app.spec.d.ts → static-ui/individual-app.spec.d.ts} +0 -0
- /package/tests/{studio/trainings/logged-in.spec.d.ts → static-ui/organizations.spec.d.ts} +0 -0
- /package/tests/{studio/trainings/logged-out.spec.d.ts → static-ui/training.spec.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#  Appsemble End 2 End Tests
|
|
2
2
|
|
|
3
3
|
> Run end 2 end tests on an Appsemble environment and provide Appsemble fixtures
|
|
4
4
|
|
|
5
|
-
[](https://gitlab.com/appsemble/appsemble/-/releases/0.35.3)
|
|
6
6
|
[](https://prettier.io)
|
|
7
7
|
|
|
8
8
|
## Table of Contents
|
|
@@ -91,5 +91,5 @@ When writing end-to-end tests, have a look at the
|
|
|
91
91
|
|
|
92
92
|
## License
|
|
93
93
|
|
|
94
|
-
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.
|
|
94
|
+
[LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.35.3/LICENSE.md) ©
|
|
95
95
|
[Appsemble](https://appsemble.com)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { expect as base } from '@playwright/test';
|
|
2
|
-
|
|
2
|
+
const availableColor = 'rgb(255, 255, 255)';
|
|
3
|
+
const inProgressColor = 'rgb(255, 127, 39)';
|
|
4
|
+
const completedColor = 'rgb(34, 177, 76)';
|
|
5
|
+
const blockedColor = 'rgb(211, 211, 211)';
|
|
3
6
|
export const expect = base.extend({
|
|
4
7
|
async toHaveChapterStatus(trainingNode, status) {
|
|
5
8
|
let backgroundColor = availableColor;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type App, type CreateAppOptions, type PatchAppOptions } from '@appsemble/types';
|
|
1
|
+
import { type App, type AppMessages, type AppsembleMessages, type CreateAppOptions, type PatchAppOptions } from '@appsemble/types';
|
|
2
2
|
export interface AppFixtures {
|
|
3
3
|
/**
|
|
4
4
|
* Create a new app for the given organization with the provided options
|
|
5
5
|
*
|
|
6
6
|
* @param organizationId ID of the organization to create the app for
|
|
7
7
|
* @param options Options to create the app with
|
|
8
|
-
* @returns
|
|
8
|
+
* @returns Created app
|
|
9
9
|
*/
|
|
10
10
|
createApp: (organizationId: string, yaml: string, options?: CreateAppOptions) => Promise<App>;
|
|
11
11
|
/**
|
|
@@ -15,6 +15,12 @@ export interface AppFixtures {
|
|
|
15
15
|
* @param options What to patch in the app
|
|
16
16
|
*/
|
|
17
17
|
patchApp: (appId: number, options: PatchAppOptions) => Promise<App>;
|
|
18
|
+
/**
|
|
19
|
+
* Delete the given app
|
|
20
|
+
*
|
|
21
|
+
* @param appId ID of the app to delete
|
|
22
|
+
*/
|
|
23
|
+
deleteApp: (appId: number) => Promise<void>;
|
|
18
24
|
/**
|
|
19
25
|
* Set the role of the user.
|
|
20
26
|
*
|
|
@@ -31,5 +37,21 @@ export interface AppFixtures {
|
|
|
31
37
|
* @returns ID of the app
|
|
32
38
|
*/
|
|
33
39
|
getAppId: (appPath: string) => Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Uploads translations for the specified language to the app
|
|
42
|
+
*
|
|
43
|
+
* @param appId ID of the app
|
|
44
|
+
* @param language The language represented by these messages
|
|
45
|
+
* @param messages The messages available to the app
|
|
46
|
+
* @returns
|
|
47
|
+
*/
|
|
48
|
+
uploadAppMessages: (appId: number, language: string, messages: AppsembleMessages) => Promise<AppMessages>;
|
|
49
|
+
/**
|
|
50
|
+
* Adds the domain of the app to the list of hosts so it can be reached in a docker environment
|
|
51
|
+
*
|
|
52
|
+
* @param appPath Path of the app
|
|
53
|
+
* @param organizationId ID of the organization that owns the app
|
|
54
|
+
*/
|
|
55
|
+
aliasAppDomain: (appPath: string, organizationId: string) => Promise<void>;
|
|
34
56
|
}
|
|
35
57
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & AppFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { appendFile } from 'node:fs/promises';
|
|
1
3
|
import { test as base } from '@playwright/test';
|
|
2
4
|
import stripIndent from 'strip-indent';
|
|
3
5
|
import { expect } from '../../expect/index.js';
|
|
4
6
|
export const test = base.extend({
|
|
5
|
-
async createApp({ request }, use) {
|
|
7
|
+
async createApp({ aliasAppDomain, request }, use) {
|
|
6
8
|
await use(async (organizationId, yaml, options) => {
|
|
7
9
|
const formData = new FormData();
|
|
8
10
|
formData.append('OrganizationId', organizationId);
|
|
@@ -22,6 +24,9 @@ export const test = base.extend({
|
|
|
22
24
|
});
|
|
23
25
|
expect(response.status()).toBe(201);
|
|
24
26
|
const app = (await response.json());
|
|
27
|
+
if (process.env.CI) {
|
|
28
|
+
await aliasAppDomain(app.path, app.OrganizationId);
|
|
29
|
+
}
|
|
25
30
|
expect(app).not.toBeNull();
|
|
26
31
|
return app;
|
|
27
32
|
});
|
|
@@ -46,6 +51,16 @@ export const test = base.extend({
|
|
|
46
51
|
return app;
|
|
47
52
|
});
|
|
48
53
|
},
|
|
54
|
+
async deleteApp({ request }, use) {
|
|
55
|
+
await use(async (appId) => {
|
|
56
|
+
const response = await request.delete(`/api/apps/${appId}`, {
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
expect(response.status()).toBe(204);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
49
64
|
async changeAppMemberRole({ browser }, use) {
|
|
50
65
|
await use(async (appId, role) => {
|
|
51
66
|
const page = await browser.newPage();
|
|
@@ -63,5 +78,29 @@ export const test = base.extend({
|
|
|
63
78
|
return appId;
|
|
64
79
|
});
|
|
65
80
|
},
|
|
81
|
+
async uploadAppMessages({ request }, use) {
|
|
82
|
+
await use(async (appId, language, messages) => {
|
|
83
|
+
const appMessages = {
|
|
84
|
+
language,
|
|
85
|
+
messages,
|
|
86
|
+
};
|
|
87
|
+
const response = await request.post(`/api/apps/${appId}/messages`, {
|
|
88
|
+
data: appMessages,
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
expect(response.status()).toBe(201);
|
|
94
|
+
return (await response.json());
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
async aliasAppDomain({ baseURL }, use) {
|
|
98
|
+
await use(async (appPath, organizationId) => {
|
|
99
|
+
const appsembleIp = String(execSync("getent hosts appsemble | awk '{ print $1 }'")).trim();
|
|
100
|
+
const baseUrl = new URL(baseURL);
|
|
101
|
+
const customAlias = `${appPath}.${organizationId}.${baseUrl.hostname}`;
|
|
102
|
+
await appendFile('/etc/hosts', `${appsembleIp} ${customAlias}\n`);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
66
105
|
});
|
|
67
106
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,22 +1,29 @@
|
|
|
1
|
+
import { type AppCollection, type AppCollectionVisibility } from '@appsemble/types';
|
|
1
2
|
export interface AppCollectionFixtures {
|
|
2
3
|
/**
|
|
3
4
|
* Creates an app collection for the given organization
|
|
4
5
|
*
|
|
5
6
|
* @param organizationId ID of the organization to create the collection for
|
|
6
7
|
* @param collectionName Name of the collection
|
|
8
|
+
* @param visibility Visibility of the collection
|
|
7
9
|
* @param expertName Name of the expert
|
|
10
|
+
* @param expertPhotoPath Path of the photo that represents the expert
|
|
11
|
+
* @param headerImagePath Path of the image that will be placed as the header of the collection
|
|
8
12
|
* @param expertDescription Description of the expert
|
|
9
|
-
* @param headerImage Image that will be placed as the header of the collection
|
|
10
|
-
* @param expertPhoto Photo that represents the expert
|
|
11
|
-
* @param isPrivate Whether the collection should be private or not
|
|
12
13
|
*/
|
|
13
|
-
createAppCollection: (organizationId: string, collectionName: string,
|
|
14
|
+
createAppCollection: (organizationId: string, collectionName: string, visibility: AppCollectionVisibility, expertName: string, expertPhotoPath: string, headerImagePath: string, expertDescription?: string) => Promise<AppCollection>;
|
|
14
15
|
/**
|
|
15
16
|
* Delete an existing app collection
|
|
16
17
|
*
|
|
17
|
-
* @param
|
|
18
|
-
|
|
18
|
+
* @param collectionId ID of the collection to delete
|
|
19
|
+
*/
|
|
20
|
+
deleteAppCollection: (collectionId: number) => Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Adds an app to an app collection
|
|
23
|
+
*
|
|
24
|
+
* @param collectionId ID of the app collection
|
|
25
|
+
* @param appId ID of the app to add
|
|
19
26
|
*/
|
|
20
|
-
|
|
27
|
+
addAppToAppCollection: (collectionId: number, appId: number) => Promise<void>;
|
|
21
28
|
}
|
|
22
29
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & AppCollectionFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
@@ -1,41 +1,55 @@
|
|
|
1
|
+
import { openAsBlob } from 'node:fs';
|
|
1
2
|
import { test as base } from '@playwright/test';
|
|
2
3
|
import { expect } from '../../../index.js';
|
|
4
|
+
async function getBlobFromPath(imagePath) {
|
|
5
|
+
const extension = imagePath.split('.').pop();
|
|
6
|
+
const blob = await openAsBlob(imagePath, { type: `image/${extension}` });
|
|
7
|
+
return blob;
|
|
8
|
+
}
|
|
3
9
|
export const test = base.extend({
|
|
4
|
-
async createAppCollection({
|
|
5
|
-
await use(async (organizationId, collectionName,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
await
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.setInputFiles(headerImage);
|
|
15
|
-
if (isPrivate) {
|
|
16
|
-
await page.getByText('Hide this app collection from the public list').click();
|
|
10
|
+
async createAppCollection({ request }, use) {
|
|
11
|
+
await use(async (organizationId, collectionName, visibility, expertName, expertPhotoPath, headerImagePath, expertDescription) => {
|
|
12
|
+
const formData = new FormData();
|
|
13
|
+
formData.append('name', collectionName);
|
|
14
|
+
formData.append('visibility', visibility);
|
|
15
|
+
formData.append('expertName', expertName);
|
|
16
|
+
formData.append('expertProfileImage', await getBlobFromPath(expertPhotoPath));
|
|
17
|
+
formData.append('headerImage', await getBlobFromPath(headerImagePath));
|
|
18
|
+
if (expertDescription) {
|
|
19
|
+
formData.append('expertDescription', expertDescription);
|
|
17
20
|
}
|
|
18
|
-
await
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await page.getByRole('button', { name: 'Create app collection' }).click();
|
|
28
|
-
await expect(page.getByRole('heading', { name: collectionName })).toBeVisible();
|
|
21
|
+
const response = await request.post(`/api/organizations/${organizationId}/app-collections`, {
|
|
22
|
+
multipart: formData,
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
expect(response.status()).toBe(201);
|
|
28
|
+
const appCollection = (await response.json());
|
|
29
|
+
return appCollection;
|
|
29
30
|
});
|
|
30
31
|
},
|
|
31
|
-
async deleteAppCollection({
|
|
32
|
-
await use(async (
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
async deleteAppCollection({ request }, use) {
|
|
33
|
+
await use(async (collectionId) => {
|
|
34
|
+
const response = await request.delete(`/api/app-collections/${collectionId}`, {
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
expect(response.status()).toBe(204);
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
async addAppToAppCollection({ request }, use) {
|
|
43
|
+
await use(async (collectionId, appId) => {
|
|
44
|
+
const response = await request.post(`/api/app-collections/${collectionId}/apps`, {
|
|
45
|
+
data: {
|
|
46
|
+
AppId: appId,
|
|
47
|
+
},
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(response.status()).toBe(204);
|
|
39
53
|
});
|
|
40
54
|
},
|
|
41
55
|
});
|
package/fixtures/test/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & import("./app/index.js").AppFixtures & import("./app-collection/index.js").AppCollectionFixtures & import("./demo-app/index.js").DemoAppFixtures & import("./group/index.js").GroupFixtures & import("./live-app/index.js").LiveAppFixtures & import("./resource/index.js").ResourceFixtures & import("./studio/index.js").StudioFixtures & import("./test-utils/index.js").TestUtilsFixtures & import("./training/index.js").TrainingFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
1
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & import("./app/index.js").AppFixtures & import("./app-collection/index.js").AppCollectionFixtures & import("./demo-app/index.js").DemoAppFixtures & import("./group/index.js").GroupFixtures & import("./live-app/index.js").LiveAppFixtures & import("./organization/index.js").OrganizationFixtures & import("./resource/index.js").ResourceFixtures & import("./studio/index.js").StudioFixtures & import("./test-utils/index.js").TestUtilsFixtures & import("./training/index.js").TrainingFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
package/fixtures/test/index.js
CHANGED
|
@@ -4,9 +4,10 @@ import { test as appCollectionFixtures } from './app-collection/index.js';
|
|
|
4
4
|
import { test as demoAppFixtures } from './demo-app/index.js';
|
|
5
5
|
import { test as groupFixtures } from './group/index.js';
|
|
6
6
|
import { test as liveAppFixtures } from './live-app/index.js';
|
|
7
|
+
import { test as organizationFixtures } from './organization/index.js';
|
|
7
8
|
import { test as resourceFixtures } from './resource/index.js';
|
|
8
9
|
import { test as studioFixtures } from './studio/index.js';
|
|
9
10
|
import { test as testUtilsFixtures } from './test-utils/index.js';
|
|
10
11
|
import { test as trainingFixtures } from './training/index.js';
|
|
11
|
-
export const test = mergeTests(appFixtures, appCollectionFixtures, demoAppFixtures, groupFixtures, liveAppFixtures, resourceFixtures, studioFixtures, testUtilsFixtures, trainingFixtures);
|
|
12
|
+
export const test = mergeTests(appFixtures, appCollectionFixtures, demoAppFixtures, groupFixtures, liveAppFixtures, organizationFixtures, resourceFixtures, studioFixtures, testUtilsFixtures, trainingFixtures);
|
|
12
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,9 +2,11 @@ export interface LiveAppFixtures {
|
|
|
2
2
|
/**
|
|
3
3
|
* Visit an app.
|
|
4
4
|
*
|
|
5
|
-
* @param
|
|
5
|
+
* @param appIdentifier Used to find the app to visit.
|
|
6
|
+
* Can be either the app's **ID** as number, or the app's **path** as string
|
|
7
|
+
* @returns URL of the live app
|
|
6
8
|
*/
|
|
7
|
-
visitApp: (
|
|
9
|
+
visitApp: (appIdentifier: number | string) => Promise<string>;
|
|
8
10
|
/**
|
|
9
11
|
* Login to an app with the Appsemble OAuth2 flow.
|
|
10
12
|
*/
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { test as base } from '@playwright/test';
|
|
2
2
|
import { expect } from '../../expect/index.js';
|
|
3
|
+
async function getAppId(appPath, request) {
|
|
4
|
+
const apps = await (await request.get('/api/apps')).json();
|
|
5
|
+
const appId = apps.find(({ path }) => path === appPath)?.id;
|
|
6
|
+
expect(appId).not.toBeUndefined();
|
|
7
|
+
return appId;
|
|
8
|
+
}
|
|
3
9
|
export const test = base.extend({
|
|
4
10
|
async visitApp({ getLiveAppURL, page, request }, use) {
|
|
5
|
-
await use(async (
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const url = await getLiveAppURL(appId);
|
|
11
|
+
await use(async (appIdentifier) => {
|
|
12
|
+
const url = await (typeof appIdentifier === 'number'
|
|
13
|
+
? getLiveAppURL(appIdentifier)
|
|
14
|
+
: getLiveAppURL(await getAppId(appIdentifier, request)));
|
|
10
15
|
await page.goto(url);
|
|
16
|
+
return url;
|
|
11
17
|
});
|
|
12
18
|
},
|
|
13
19
|
async loginAppAppsembleOAuth({ page }, use) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Organization } from '@appsemble/types';
|
|
2
|
+
interface OrganizationDetails {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
website?: string;
|
|
8
|
+
icon?: Blob;
|
|
9
|
+
}
|
|
10
|
+
export interface OrganizationFixtures {
|
|
11
|
+
/**
|
|
12
|
+
* Creates an organization
|
|
13
|
+
*
|
|
14
|
+
* @param details Organization details
|
|
15
|
+
* @returns Created organization
|
|
16
|
+
*/
|
|
17
|
+
createOrganization: (details: OrganizationDetails) => Promise<Organization>;
|
|
18
|
+
/**
|
|
19
|
+
* Deletes the organization with the given ID
|
|
20
|
+
*
|
|
21
|
+
* @param id ID of the organization
|
|
22
|
+
*/
|
|
23
|
+
deleteOrganization: (id: string) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & OrganizationFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test as base } from '@playwright/test';
|
|
2
|
+
import { expect } from '../../expect/index.js';
|
|
3
|
+
export const test = base.extend({
|
|
4
|
+
async createOrganization({ request }, use) {
|
|
5
|
+
await use(async (details) => {
|
|
6
|
+
const { description, email, icon, id, name, website } = details;
|
|
7
|
+
const formData = new FormData();
|
|
8
|
+
formData.set('id', id);
|
|
9
|
+
formData.set('name', name ?? '');
|
|
10
|
+
formData.set('description', description ?? '');
|
|
11
|
+
formData.set('email', email ?? '');
|
|
12
|
+
formData.set('website', website ?? 'http');
|
|
13
|
+
formData.set('icon', icon ?? null);
|
|
14
|
+
const response = await request.post('/api/organizations', {
|
|
15
|
+
multipart: formData,
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
expect(response.status()).toBe(201);
|
|
21
|
+
return response.json();
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
async deleteOrganization({ request }, use) {
|
|
25
|
+
await use(async (id) => {
|
|
26
|
+
const response = await request.delete(`/api/organizations/${id}`, {
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
expect(response.status()).toBe(200);
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Page } from '@playwright/test';
|
|
1
2
|
export interface StudioFixtures {
|
|
2
3
|
/**
|
|
3
4
|
* Log into Appsemble studio as a user
|
|
@@ -9,9 +10,40 @@ export interface StudioFixtures {
|
|
|
9
10
|
* @returns The user's access token
|
|
10
11
|
*/
|
|
11
12
|
loginUser: (email: string, password: string, saveInStorageState?: boolean) => Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Log into Appsemble studio as a user using the given page
|
|
15
|
+
*
|
|
16
|
+
* @param email Email address of the account to log in with
|
|
17
|
+
* @param password Password of the account
|
|
18
|
+
* @param page Page to login with
|
|
19
|
+
* @param saveInStorageState Whether or not to save the authenticated state as reusable storage
|
|
20
|
+
* state (see: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests)
|
|
21
|
+
* @returns The user's access token
|
|
22
|
+
*/
|
|
23
|
+
loginUserOnPage: (email: string, password: string, page: Page, saveInStorageState?: boolean) => Promise<string>;
|
|
12
24
|
/**
|
|
13
25
|
* Log the worker out of their current user account
|
|
14
26
|
*/
|
|
15
27
|
logoutUser: () => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Clicks on one of the items in the app side menu in the studio
|
|
30
|
+
*
|
|
31
|
+
* @param itemName Name of the menu item to click
|
|
32
|
+
*/
|
|
33
|
+
clickAppSideMenuItem: (itemName: string) => Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Clicks on one of the items in the app side menu in the studio
|
|
36
|
+
*
|
|
37
|
+
* @param itemName Name of the menu item to click
|
|
38
|
+
* @param page Page to use
|
|
39
|
+
*/
|
|
40
|
+
clickAppSideMenuItemOnPage: (itemName: string, page: Page) => Promise<void>;
|
|
16
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Clicks on one of the items in the app side menu in the studio using the given page
|
|
44
|
+
*
|
|
45
|
+
* @param page Page on which to click
|
|
46
|
+
* @param itemName Name of the menu item to click
|
|
47
|
+
*/
|
|
48
|
+
export declare function clickAppSideMenuItem(page: Page, itemName: string): Promise<void>;
|
|
17
49
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & StudioFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { test as base, expect } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Clicks on one of the items in the app side menu in the studio using the given page
|
|
4
|
+
*
|
|
5
|
+
* @param page Page on which to click
|
|
6
|
+
* @param itemName Name of the menu item to click
|
|
7
|
+
*/
|
|
8
|
+
export async function clickAppSideMenuItem(page, itemName) {
|
|
9
|
+
await page.getByTestId('studio-app-side-menu').getByRole('link', { name: itemName }).click();
|
|
10
|
+
}
|
|
2
11
|
export const test = base.extend({
|
|
3
|
-
async loginUser({ page }, use) {
|
|
12
|
+
async loginUser({ loginUserOnPage, page }, use) {
|
|
4
13
|
await use(async (email, password, saveInStorageState = false) => {
|
|
14
|
+
const accessToken = await loginUserOnPage(email, password, page, saveInStorageState);
|
|
15
|
+
return accessToken;
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
async loginUserOnPage({}, use) {
|
|
19
|
+
await use(async (email, password, page, saveInStorageState = false) => {
|
|
5
20
|
let accessToken = '';
|
|
6
21
|
// Get the access token from the "login" response
|
|
7
22
|
page.on('response', async (response) => {
|
|
@@ -29,5 +44,15 @@ export const test = base.extend({
|
|
|
29
44
|
await page.getByRole('button', { name: 'Logout' }).click();
|
|
30
45
|
});
|
|
31
46
|
},
|
|
47
|
+
async clickAppSideMenuItem({ clickAppSideMenuItemOnPage, page }, use) {
|
|
48
|
+
await use(async (itemName) => {
|
|
49
|
+
await clickAppSideMenuItemOnPage(itemName, page);
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
async clickAppSideMenuItemOnPage({}, use) {
|
|
53
|
+
await use(async (itemName, page) => {
|
|
54
|
+
await page.getByTestId('studio-app-side-menu').getByRole('link', { name: itemName }).click();
|
|
55
|
+
});
|
|
56
|
+
},
|
|
32
57
|
});
|
|
33
58
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,9 +2,13 @@ export interface TestUtilsFixtures {
|
|
|
2
2
|
/**
|
|
3
3
|
* Generates a random test ID based on details of the current test
|
|
4
4
|
*
|
|
5
|
+
* @param level The level of detail to add. Each level adds more detail\
|
|
6
|
+
* 1 = Browser name\
|
|
7
|
+
* 2 = Worker index\
|
|
8
|
+
* 3 = Retry count
|
|
5
9
|
* @returns Randomized test ID
|
|
6
10
|
* @example '[chrome][index=1][retry=3]-123456'
|
|
7
11
|
*/
|
|
8
|
-
randomTestId: () => string;
|
|
12
|
+
randomTestId: (level?: 1 | 2 | 3) => string;
|
|
9
13
|
}
|
|
10
14
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & TestUtilsFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { test as base } from '@playwright/test';
|
|
2
2
|
export const test = base.extend({
|
|
3
3
|
randomTestId({ browserName }, use, testInfo) {
|
|
4
|
-
use(() =>
|
|
4
|
+
use((level) => {
|
|
5
|
+
let prefix = '';
|
|
6
|
+
if (level) {
|
|
7
|
+
if (level >= 1) {
|
|
8
|
+
prefix = prefix.concat(`[${browserName}]`);
|
|
9
|
+
}
|
|
10
|
+
if (level >= 2) {
|
|
11
|
+
prefix = prefix.concat(`[index=${testInfo.workerIndex}]`);
|
|
12
|
+
}
|
|
13
|
+
if (level >= 3) {
|
|
14
|
+
prefix = prefix.concat(`[retry=${testInfo.retry}]`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return `${prefix}${prefix === '' ? '' : '-'}${Math.floor(100000 + Math.random() * 900000)}`;
|
|
18
|
+
});
|
|
5
19
|
},
|
|
6
20
|
});
|
|
7
21
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"description": "End-to-end tests used for testing Appsemble apps in an existing environment.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"test": "vitest"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@appsemble/utils": "0.
|
|
42
|
-
"@appsemble/types": "0.
|
|
43
|
-
"@appsemble/lang-sdk": "0.
|
|
41
|
+
"@appsemble/utils": "0.35.3",
|
|
42
|
+
"@appsemble/types": "0.35.3",
|
|
43
|
+
"@appsemble/lang-sdk": "0.35.3",
|
|
44
44
|
"jwt-decode": "^4.0.0",
|
|
45
45
|
"@fortawesome/fontawesome-free": "^6.0.0",
|
|
46
46
|
"@playwright/test": "1.51.1",
|
package/playwright.config.js
CHANGED
|
@@ -11,10 +11,12 @@ export default defineConfig({
|
|
|
11
11
|
forbidOnly: Boolean(CI),
|
|
12
12
|
// Retry on CI only
|
|
13
13
|
retries: CI ? 2 : 0,
|
|
14
|
-
//
|
|
14
|
+
// Concurrent tests run better on CI in general
|
|
15
15
|
workers: 1,
|
|
16
16
|
// Reporter to use. See https://playwright.dev/docs/test-reporters
|
|
17
17
|
reporter: [['junit', { outputFile: 'results.xml' }]],
|
|
18
|
+
// Prevent the pipeline from timing out
|
|
19
|
+
maxFailures: 6,
|
|
18
20
|
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
|
|
19
21
|
use: {
|
|
20
22
|
// Base URL to use in actions like `await page.goto('/')`.
|
|
@@ -30,6 +32,24 @@ export default defineConfig({
|
|
|
30
32
|
},
|
|
31
33
|
// Configure projects for major browsers
|
|
32
34
|
projects: [
|
|
35
|
+
{
|
|
36
|
+
name: 'static-chromium',
|
|
37
|
+
testDir: './tests/static-ui',
|
|
38
|
+
use: { ...devices['Desktop Chrome'] },
|
|
39
|
+
fullyParallel: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'static-firefox',
|
|
43
|
+
testDir: './tests/static-ui',
|
|
44
|
+
use: { ...devices['Desktop Firefox'] },
|
|
45
|
+
fullyParallel: true,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'static-safari',
|
|
49
|
+
testDir: './tests/static-ui',
|
|
50
|
+
use: { ...devices['Desktop Safari'] },
|
|
51
|
+
fullyParallel: true,
|
|
52
|
+
},
|
|
33
53
|
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
|
34
54
|
{
|
|
35
55
|
name: 'chromium',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { expect, test } from '../../index.js';
|
|
3
|
+
let appId;
|
|
4
|
+
let organizationId;
|
|
5
|
+
test.describe('Empty', () => {
|
|
6
|
+
test.beforeAll(async ({ createApp, createOrganization, randomTestId }) => {
|
|
7
|
+
const appDefinition = await readFile('../../apps/empty/app-definition.yaml', 'utf8');
|
|
8
|
+
organizationId = (await createOrganization({ id: randomTestId() })).id;
|
|
9
|
+
appId = (await createApp(organizationId, appDefinition)).id;
|
|
10
|
+
});
|
|
11
|
+
test.afterAll(async ({ deleteApp, deleteOrganization }) => {
|
|
12
|
+
await deleteApp(appId);
|
|
13
|
+
await deleteOrganization(organizationId);
|
|
14
|
+
});
|
|
15
|
+
test.beforeEach(async ({ page, visitApp }) => {
|
|
16
|
+
await visitApp(appId);
|
|
17
|
+
await page.waitForURL('**/example-page-a');
|
|
18
|
+
});
|
|
19
|
+
test('should navigate to the second page and back', async ({ page }) => {
|
|
20
|
+
await page.click('a.button');
|
|
21
|
+
await expect(page.getByRole('heading', { name: 'Example Page B' })).toBeVisible();
|
|
22
|
+
await page.click('a.button');
|
|
23
|
+
await expect(page.getByRole('heading', { name: 'Example Page A' })).toBeVisible();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=empty.spec.js.map
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import { expect, test } from '../../index.js';
|
|
3
|
+
let appId;
|
|
4
|
+
let organizationId;
|
|
2
5
|
test.describe('Holidays', () => {
|
|
6
|
+
test.beforeAll(async ({ createApp, createOrganization, randomTestId }) => {
|
|
7
|
+
const appDefinition = await readFile('../../apps/holidays/app-definition.yaml', 'utf8');
|
|
8
|
+
organizationId = (await createOrganization({ id: randomTestId() })).id;
|
|
9
|
+
appId = (await createApp(organizationId, appDefinition)).id;
|
|
10
|
+
});
|
|
11
|
+
test.afterAll(async ({ deleteApp, deleteOrganization }) => {
|
|
12
|
+
await deleteApp(appId);
|
|
13
|
+
await deleteOrganization(organizationId);
|
|
14
|
+
});
|
|
3
15
|
test.beforeEach(async ({ page, visitApp }) => {
|
|
4
16
|
await page.route('/api/apps/*/actions/pages.0.tabs.0.blocks.0.actions.onLoad*', (route) => {
|
|
5
17
|
route.fulfill({ path: 'mock-data/holidays-nl.json' });
|
|
@@ -10,7 +22,7 @@ test.describe('Holidays', () => {
|
|
|
10
22
|
await page.route('/api/apps/*/actions/pages.1.tabs.0.blocks.0.actions.onLoad*', (route) => {
|
|
11
23
|
route.fulfill({ path: 'mock-data/holidays-us.json' });
|
|
12
24
|
});
|
|
13
|
-
await visitApp(
|
|
25
|
+
await visitApp(appId);
|
|
14
26
|
await page.waitForURL('**/holidays-in-europe/netherlands');
|
|
15
27
|
});
|
|
16
28
|
test('should navigate to the second tab', async ({ page }) => {
|