@axinom/mosaic-e2e-page-model 0.2.5-rc.9 → 0.3.0-rc.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-e2e-page-model",
3
- "version": "0.2.5-rc.9",
3
+ "version": "0.3.0-rc.1",
4
4
  "description": "A page model for testing an Axinom Mosaic Management System with Playwright.",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -10,7 +10,8 @@
10
10
  "axinom mosaic"
11
11
  ],
12
12
  "files": [
13
- "dist"
13
+ "dist",
14
+ "src"
14
15
  ],
15
16
  "main": "dist/index.js",
16
17
  "types": "dist/index.d.ts",
@@ -21,7 +22,7 @@
21
22
  "dev": "tsc -w"
22
23
  },
23
24
  "dependencies": {
24
- "@axinom/mosaic-e2e-ui-selectors": "^0.2.5-rc.9"
25
+ "@axinom/mosaic-e2e-ui-selectors": "^0.3.0-rc.1"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/node": "^14.18.0",
@@ -34,5 +35,5 @@
34
35
  "publishConfig": {
35
36
  "access": "public"
36
37
  },
37
- "gitHead": "c9dfcc273fab65d29108afc4dcf62db978afbd9b"
38
+ "gitHead": "ead0510054dde1f3bc61b39a57e83e541d251b59"
38
39
  }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './managed-services';
2
+ export * from './page-model';
3
+ export * from './service-model';
4
+ export * from './sign-in';
5
+ export * from './ui-selectors';
@@ -0,0 +1,231 @@
1
+ import { ManagedServiceModel } from './managed-service-model';
2
+
3
+ export class ChannelService extends ManagedServiceModel {
4
+ /**
5
+ * This method creates a channel by title.
6
+ * The management system must be loaded & signed in before using this method.
7
+ *
8
+ * The following steps will be taken:
9
+ * - Navigate to the channels station
10
+ * - Click on [New] button
11
+ * - Populate channel's title and click on [Proceed] button in order to save newly created channel
12
+ *
13
+ * @param args.channelTitle The title of the channel
14
+ */
15
+ async createChannel(args: { channelTitle: string }): Promise<void> {
16
+ const { uiShell, uiManagedWorkflows } = this;
17
+
18
+ // create channel
19
+ await uiShell.navigateToLandingPageTile('Channels');
20
+ await uiManagedWorkflows.list.waitForData();
21
+ await uiShell.waitForPageTransition(
22
+ uiManagedWorkflows.pageHeader.actions.getActionByLabel('NEW').click(),
23
+ );
24
+ await uiManagedWorkflows.form
25
+ .getFieldByLabel('Title')
26
+ .asSingleLineTextField()
27
+ .setValue(args.channelTitle);
28
+ await uiShell.waitForPageTransition(
29
+ uiManagedWorkflows.actions.getActionByLabel('Proceed').click(),
30
+ );
31
+ await uiManagedWorkflows.form.waitForData();
32
+ }
33
+
34
+ /**
35
+ * This method opens channel properties page for a channel.
36
+ * The management system must be loaded & signed in before using this method.
37
+ *
38
+ * The following steps will be taken:
39
+ * - Navigate to the channels station
40
+ * - Filter out for the particular channel by title
41
+ * - Open channel properties page
42
+ *
43
+ * @param args.channelTitle The title of the channel
44
+ */
45
+ async navigateToChannelProperties(args: {
46
+ channelTitle: string;
47
+ }): Promise<void> {
48
+ const { uiShell, uiManagedWorkflows } = this;
49
+
50
+ // filter out newly created channel
51
+ await uiShell.navigateToLandingPageTile('Channels');
52
+ await uiManagedWorkflows.list.waitForData();
53
+ await uiManagedWorkflows.filters
54
+ .getFilterByName('title')
55
+ .asFreeTextFilter()
56
+ .setValue(args.channelTitle);
57
+ await uiManagedWorkflows.list.waitForData();
58
+
59
+ // navigate to the channel properties station
60
+ await uiShell.waitForPageTransition(
61
+ uiManagedWorkflows.list.getRow(1).actionButton.click(),
62
+ );
63
+ await uiManagedWorkflows.form.waitForData();
64
+ }
65
+
66
+ /**
67
+ * This method publishes a channel.
68
+ * The management system must be loaded & signed in before using this method.
69
+ *
70
+ * The following steps will be taken:
71
+ * - Navigate to the channels station
72
+ * - Filter out for the particular channel by title
73
+ * - Open channel properties page
74
+ * - Publish the channel
75
+ *
76
+ * @param args.channelTitle The title of the channel
77
+ */
78
+ async publishChannel(args: { channelTitle: string }): Promise<void> {
79
+ const { uiShell, uiManagedWorkflows } = this;
80
+
81
+ this.navigateToChannelProperties({
82
+ channelTitle: args.channelTitle,
83
+ });
84
+
85
+ // click on [Publishing] button
86
+ await uiManagedWorkflows.form.waitForData();
87
+ await uiShell.waitForPageTransition(
88
+ uiManagedWorkflows.actions.getActionByLabel('Publishing').click(),
89
+ );
90
+ // publish the channel
91
+ await uiManagedWorkflows.actions.getActionByLabel('Publish').click(),
92
+ await uiShell.waitForPageTransition(
93
+ uiManagedWorkflows.actions.confirmButton.click(),
94
+ );
95
+ }
96
+
97
+ /**
98
+ * This method unpublishes a channel.
99
+ * The management system must be loaded & signed in before using this method.
100
+ *
101
+ * The following steps will be taken:
102
+ * - Navigate to the channels station
103
+ * - Filter out for the particular channel by title
104
+ * - Open channel properties page
105
+ * - Unpublish the channel
106
+ *
107
+ * @param args.channelTitle The title of the channel
108
+ */
109
+ async unpublishChannel(args: { channelTitle: string }): Promise<void> {
110
+ const { uiShell, uiManagedWorkflows } = this;
111
+
112
+ this.navigateToChannelProperties({
113
+ channelTitle: args.channelTitle,
114
+ });
115
+
116
+ await uiManagedWorkflows.actions.getActionByLabel('Unpublish').click(),
117
+ await uiShell.waitForPageTransition(
118
+ uiManagedWorkflows.actions.confirmButton.click(),
119
+ );
120
+ }
121
+
122
+ /**
123
+ * This method deletes a channel.
124
+ * The management system must be loaded & signed in before using this method.
125
+ *
126
+ * The following steps will be taken:
127
+ * - Navigate to the channels station
128
+ * - Filter out for the particular channel by title
129
+ * - Open channel properties page
130
+ * - Delete the channel
131
+ *
132
+ * @param args.channelTitle The title of the channel
133
+ */
134
+ async deleteChannel(args: { channelTitle: string }): Promise<void> {
135
+ const { uiShell, uiManagedWorkflows } = this;
136
+
137
+ this.navigateToChannelProperties({
138
+ channelTitle: args.channelTitle,
139
+ });
140
+
141
+ const action = uiManagedWorkflows.actions.getActionByLabel('Delete');
142
+ try {
143
+ await action.waitFor({ state: 'visible' });
144
+ } catch {
145
+ throw new Error(`The "Delete" action is not visible.`);
146
+ }
147
+ await action.click();
148
+ await uiShell.waitForPageTransition(
149
+ uiManagedWorkflows.actions.confirmButton.click(),
150
+ );
151
+ await uiManagedWorkflows.list.waitForData();
152
+ }
153
+
154
+ /**
155
+ * This method creates a playlist for a channel.
156
+ * The management system must be loaded & signed in before using this method.
157
+ *
158
+ * The following steps will be taken:
159
+ * - Navigate to the channels station
160
+ * - Filter out for the particular channel by title
161
+ * - Open channel properties page
162
+ * - Click on [Playlists] button
163
+ * - Click on [New] button on the Playlists station
164
+ * - Populate Scheduled Start date and proceed in order to save newly created playlist
165
+ *
166
+ * @param args.channelTitle The title of the channel
167
+ * @param args.scheduledStart The start date/time of the playlist
168
+ */
169
+ async createPlaylist(args: {
170
+ channelTitle: string;
171
+ scheduledStart: string;
172
+ }): Promise<void> {
173
+ const { uiShell, uiManagedWorkflows } = this;
174
+
175
+ this.navigateToChannelProperties({
176
+ channelTitle: args.channelTitle,
177
+ });
178
+ await uiShell.waitForPageTransition(
179
+ uiManagedWorkflows.actions.getActionByLabel('Playlists').click(),
180
+ );
181
+
182
+ // create playlist
183
+ await uiShell.waitForPageTransition(
184
+ uiManagedWorkflows.pageHeader.actions.getActionByLabel('NEW').click(),
185
+ );
186
+ await uiManagedWorkflows.form
187
+ .getFieldByLabel('Scheduled Start')
188
+ .asDateTimeField()
189
+ .setValue(args.scheduledStart);
190
+ await uiShell.waitForPageTransition(
191
+ uiManagedWorkflows.actions.getActionByLabel('Proceed').click(),
192
+ );
193
+ }
194
+
195
+ /**
196
+ * This method publishes a playlist.
197
+ * The management system must be loaded & signed in before using this method.
198
+ *
199
+ * The following steps will be taken:
200
+ * - Navigate to the channels station
201
+ * - Filter out for the particular channel by title
202
+ * - Open channel properties page
203
+ * - Click on [Playlists] button
204
+ * - Click on first playlist found
205
+ * - Publish the playlist
206
+ *
207
+ * @param args.channelTitle The title of the channel
208
+ */
209
+ async publishPlaylist(args: { channelTitle: string }): Promise<void> {
210
+ const { uiShell, uiManagedWorkflows } = this;
211
+
212
+ this.navigateToChannelProperties({
213
+ channelTitle: args.channelTitle,
214
+ });
215
+
216
+ // publish playlist
217
+ await uiShell.waitForPageTransition(
218
+ uiManagedWorkflows.actions.getActionByLabel('Playlists').click(),
219
+ );
220
+ await uiShell.waitForPageTransition(
221
+ uiManagedWorkflows.list.getRow(1).actionButton.click(),
222
+ );
223
+ await uiShell.waitForPageTransition(
224
+ uiManagedWorkflows.actions.getActionByLabel('Publishing').click(),
225
+ );
226
+ await uiManagedWorkflows.actions.getActionByLabel('Publish').click(),
227
+ await uiShell.waitForPageTransition(
228
+ uiManagedWorkflows.actions.confirmButton.click(),
229
+ );
230
+ }
231
+ }
@@ -0,0 +1,40 @@
1
+ import { SelectionExplorer } from '@axinom/mosaic-e2e-ui-selectors';
2
+
3
+ /** A mod*e*l for the mod*a*l image explorer component used to select images. */
4
+ export class ImageSelectionExplorer extends SelectionExplorer {
5
+ /**
6
+ * Select an image by title.
7
+ * If the image explorer modal is not showing or the selected title does not exist then an exception will be raised.
8
+ *
9
+ * The following steps will be taken:
10
+ * - Verify that the page header title is 'Select Image'
11
+ * - Filter by title and verify that at least one result is shown
12
+ * - Select the first result by row action
13
+ * - Wait for the modal to be closed
14
+ */
15
+ async selectImageByTitle(imageTitle: string): Promise<void> {
16
+ // verify the modal header
17
+ await this.pageHeader.title.waitFor({ state: 'visible' });
18
+ if ((await this.pageHeader.title.textContent()) !== 'Select Image') {
19
+ throw new Error('Image explorer modal must be showing.');
20
+ }
21
+ await this.waitToOpen();
22
+ await this.list.waitForData();
23
+
24
+ await this.filters
25
+ .getFilterByName('title')
26
+ .asFreeTextFilter()
27
+ .setValue(imageTitle);
28
+ await this.list.waitForData();
29
+
30
+ // verify that one result is showing
31
+ if ((await this.list.allVisibleRows.count()) === 0) {
32
+ throw new Error('An image with the specified title was not found.');
33
+ }
34
+
35
+ await this.list.getRow(1).selectButton.click();
36
+
37
+ // wait for the modal to close
38
+ await this.waitToClose();
39
+ }
40
+ }
@@ -0,0 +1,142 @@
1
+ import { ActionLabel } from '@axinom/mosaic-e2e-ui-selectors';
2
+ import { ManagedServiceModel } from '../managed-service-model';
3
+ import { ImageSelectionExplorer } from './image-selection-explorer';
4
+
5
+ /** A model for the managed image service. */
6
+ export class ImageService extends ManagedServiceModel {
7
+ /**
8
+ * This method navigates to an image details station by title.
9
+ * The management system must be loaded & signed in before using this method.
10
+ * If there is not exactly one matching image an exception will be raised.
11
+ *
12
+ * The following steps will be taken:
13
+ * - Navigate to the home breadcrumb then the 'Images' tile
14
+ * - Filter by title and click the image row action
15
+ * - Wait for the image details station to load
16
+ */
17
+ async navigateToImageDetails(args: {
18
+ /** A unique image title. */
19
+ title: string;
20
+ }): Promise<void> {
21
+ const { uiShell, uiManagedWorkflows } = this;
22
+ await uiShell.navigateToLandingPageTile('Images');
23
+ await uiManagedWorkflows.list.waitForData();
24
+
25
+ // filter to the item
26
+ await uiManagedWorkflows.filters
27
+ .getFilterByName('title')
28
+ .asFreeTextFilter()
29
+ .setValue(args.title);
30
+ await uiManagedWorkflows.list.waitForData();
31
+
32
+ // Verify that there is exactly one matching row
33
+ if ((await uiManagedWorkflows.list.allVisibleRows.count()) !== 1) {
34
+ throw new Error(
35
+ `Failed to find exactly one non-archived image with title '${args.title}'.`,
36
+ );
37
+ }
38
+
39
+ // navigate
40
+ await uiShell.waitForPageTransition(
41
+ uiManagedWorkflows.list.getRow(1).actionButton.click(),
42
+ );
43
+ await uiManagedWorkflows.form.waitForData();
44
+ }
45
+
46
+ /**
47
+ * This method uploads an image file from the local file system.
48
+ * The management system must be loaded & signed in before using this method.
49
+ * A unique image title and a valid image type must be provided.
50
+ *
51
+ * The following steps will be taken:
52
+ * - Navigate to the home breadcrumb then the 'Images' tile
53
+ * - Click the 'Upload' action
54
+ * - Enter file and image type inputs then click 'Proceed'
55
+ * - Edit the image title
56
+ * - Click the 'refresh' breadcrumb and wait for the image details station to reload
57
+ */
58
+ async uploadImage(properties: {
59
+ /** Path to a local image file. */
60
+ sourceFile: string;
61
+ /** A unique image title. */
62
+ title: string;
63
+ /** An existing image type (display value). */
64
+ imageType: string;
65
+ }): Promise<void> {
66
+ const { uiShell, uiManagedWorkflows } = this;
67
+ await uiShell.navigateToLandingPageTile('Images');
68
+ await uiShell.waitForPageTransition(
69
+ uiManagedWorkflows.pageHeader.actions.getActionByLabel('Upload').click(),
70
+ );
71
+
72
+ // Setting image upload input
73
+ await uiManagedWorkflows.form
74
+ .getFieldByName('file')
75
+ .asFileUploadField()
76
+ .setValue(properties.sourceFile);
77
+
78
+ // Set image type.
79
+ await uiManagedWorkflows.form
80
+ .getFieldByName('imageType')
81
+ .asSelectField()
82
+ .setValueByLabel(properties.imageType);
83
+
84
+ // Submitting the upload
85
+ await uiShell.waitForPageTransition(
86
+ uiManagedWorkflows.actions.getActionByLabel(ActionLabel.Proceed).click(),
87
+ );
88
+ await uiManagedWorkflows.form.waitForData();
89
+
90
+ // Set image title.
91
+ await uiManagedWorkflows.form
92
+ .getFieldByName('title')
93
+ .asSingleLineTextField()
94
+ .setValue(properties.title);
95
+
96
+ // Refresh to save
97
+ await uiShell.navigateToBreadcrumbRefresh();
98
+ await uiManagedWorkflows.form.waitForData();
99
+ }
100
+
101
+ /**
102
+ * This method archives an image by title.
103
+ * The management system must be loaded & signed in before using this method.
104
+ * If a single matching, non-archived image is not found then an exception will be raised.
105
+ *
106
+ * The following steps will be taken:
107
+ * - Navigate to the home breadcrumb then the 'Images' tile
108
+ * - Filter by title and click the image row action
109
+ * - Click 'Archive' and confirm
110
+ * - Wait for the image explorer station to load
111
+ */
112
+ async archiveImage(args: {
113
+ /** A unique image title. */
114
+ title: string;
115
+ }): Promise<void> {
116
+ const { uiShell, uiManagedWorkflows } = this;
117
+ await this.navigateToImageDetails(args);
118
+ try {
119
+ await uiManagedWorkflows.actions
120
+ .getActionByLabel('Archive')
121
+ .waitFor({ state: 'visible' });
122
+ } catch {
123
+ throw new Error(`The image archive button is not visible.`);
124
+ }
125
+ await uiManagedWorkflows.actions.getActionByLabel('Archive').click();
126
+ await uiShell.waitForPageTransition(
127
+ uiManagedWorkflows.actions.confirmButton.click(),
128
+ );
129
+ await uiManagedWorkflows.list.waitForData();
130
+ }
131
+
132
+ /**
133
+ * A reference to the ImageSelectionExplorer component mod*e*l which can be used
134
+ * to select images in a mod*a*l. e.g.
135
+ * ```
136
+ * await app.ui.form.modal
137
+ * .as(app.imageService.ImageSelectionExplorer)
138
+ * .selectImage('My image title');
139
+ * ```
140
+ */
141
+ readonly ImageSelectionExplorer = ImageSelectionExplorer;
142
+ }
@@ -0,0 +1,2 @@
1
+ export * from './image-selection-explorer';
2
+ export * from './image-service';
@@ -0,0 +1,4 @@
1
+ export * from './channel-service';
2
+ export * from './image-service';
3
+ export * from './monetization-service';
4
+ export * from './video-service';
@@ -0,0 +1,31 @@
1
+ import { UiWorkflowsModel } from '@axinom/mosaic-e2e-ui-selectors';
2
+ import { Page } from 'playwright-core';
3
+ import { UiShellModel } from '../ui-selectors/ui-shell-model';
4
+
5
+ /**
6
+ * An abstract model representing a Mosaic managed service.
7
+ */
8
+ export abstract class ManagedServiceModel {
9
+ constructor(
10
+ /** The playwright page object. */
11
+ protected readonly page: Page,
12
+
13
+ /**
14
+ * A model representing the Mosaic UI shell including methods for navigation.
15
+ *
16
+ * Use this model to interact with:
17
+ * - The breadcrumbs bar (on any page)
18
+ * - The landing page
19
+ * - The settings hub page
20
+ */
21
+ protected readonly uiShell: UiShellModel,
22
+
23
+ /**
24
+ * A model representing Mosaic UI workflows.
25
+ *
26
+ * Use this model to interact with workflows or mod*a*ls of any managed
27
+ * service (Image, Video, Monetization, etc).
28
+ */
29
+ protected readonly uiManagedWorkflows: UiWorkflowsModel,
30
+ ) {}
31
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ Accordion,
3
+ AccordionItem,
4
+ ComponentModel,
5
+ DynamicDataList,
6
+ Modal,
7
+ } from '@axinom/mosaic-e2e-ui-selectors';
8
+ import { FrameLocator, Locator, Page } from 'playwright-core';
9
+
10
+ /**
11
+ * A model for the a claim row in the claims editor station. This model should not be used directly.
12
+ * It is used internally by methods of the `MonetizationService` model.
13
+ */
14
+ export class ClaimRow extends ComponentModel {
15
+ /** Get a locator to the appropriate input element to select this claim. */
16
+ async getInput(): Promise<Locator> {
17
+ const radioLabel = this.getLocator('//label', -1);
18
+ const checkBox = this.getLocator('//input', -1);
19
+ return (await radioLabel.isVisible()) ? radioLabel : checkBox;
20
+ }
21
+
22
+ /** This method toggles the claim selection. */
23
+ async toggle(): Promise<void> {
24
+ const input = await this.getInput();
25
+ await input.check();
26
+ }
27
+ }
28
+
29
+ /**
30
+ * A model for the a claim group in the claims editor station. This model should not be used directly.
31
+ * It is used internally by methods of the `MonetizationService` model.
32
+ */
33
+ export class ClaimGroup extends AccordionItem {
34
+ /** A model for the dynamic data list used to for claim multiple select. */
35
+ readonly dataList = new DynamicDataList(this);
36
+
37
+ /** A model for the modal opened by the dynamic data list. */
38
+ readonly explorerModal = new Modal(this).asSelectionExplorer();
39
+
40
+ /**
41
+ * This method will determine whether the claim group uses modal input.
42
+ * If this returns true, then claims should be selected with the `dataList` and `explorerModal` properties.
43
+ * If it returns false then the `getClaimRowByLabel` method should be used.
44
+ */
45
+ async isDataListEntry(): Promise<boolean> {
46
+ return this.dataList.getLocator().isVisible();
47
+ }
48
+
49
+ /** This method gets a claim row by label. */
50
+ getClaimRowByLabel(claimLabel: string): ClaimRow {
51
+ return new ClaimRow(
52
+ this,
53
+ `//*[contains(@data-test-id, "claim:") and .//*[@data-test-id="claim-label" and text()="${claimLabel}"]]`,
54
+ );
55
+ }
56
+ }
57
+
58
+ /**
59
+ * A model for the claims editor station. This model should not be used directly.
60
+ * It is used internally by methods of the `MonetizationService` model.
61
+ */
62
+ export class ClaimsEditor extends ComponentModel {
63
+ /**
64
+ * A model for an accordion component.
65
+ * @param parent The parent playwright page, frame or component model.
66
+ * @param xpath A relative xpath selector to this element from the parent.
67
+ */
68
+ constructor(parent: Page | FrameLocator | ComponentModel, xpath = '//form') {
69
+ super(parent, xpath);
70
+ }
71
+
72
+ private accordion = new Accordion(this.root, this.xpath);
73
+
74
+ /** This method gets a claim row by claim value. */
75
+ getClaimRow(claim: string): ClaimRow {
76
+ return new ClaimRow(this.accordion, `//*[@data-test-id="claim:${claim}"]`);
77
+ }
78
+
79
+ /** This method gets a claim group by label. */
80
+ getClaimGroup(claimGroupLabel: string): ClaimGroup {
81
+ const accordionItem = this.accordion.getItemContainingText(claimGroupLabel);
82
+ return new ClaimGroup(this.root, accordionItem.xpath);
83
+ }
84
+ }
@@ -0,0 +1,3 @@
1
+ export * from './monetization-service';
2
+ export * from './payment-plan-recurrence-period-field';
3
+ export * from './payment-provider-settings-field';