Skip to content

Commit 1f95c06

Browse files
authored
Fix part of oppia#17712: Add Voiceover admin acceptance tests (oppia#19994)
* acceptance for voiceover admin * checkpoint * fixed first test * fixed second test * first pass review * updated expectVoiceoverArtistsToContain to be plural * updated expectVoiceoverArtistsToContain to be plural * second pass review * third pass review * top level-test * top level-test * fixed invalidId test * fixed invalidId test * mobile tests * updated error * explorationEditor utility * mobile test * rename expectVoiceoverArtistsListDoesNotContain
1 parent a338cd1 commit 1f95c06

File tree

9 files changed

+495
-6
lines changed

9 files changed

+495
-6
lines changed

.github/workflows/e2e_lighthouse_performance_acceptance_tests.yml

+1
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ jobs:
321321
- practice-question-admin-tests/add-and-remove-contribution-rights
322322
- translation-admin-tests/add-translation-rights
323323
- translation-admin-tests/remove-translation-rights
324+
- voiceover-admin-tests/add-voiceover-artist-to-an-exploration
324325
steps:
325326
- uses: actions/checkout@v3
326327
- uses: actions/setup-python@v3

core/templates/pages/exploration-editor-page/exploration-editor-page.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ <h1>{{ getNavbarText() }}</h1>
8080

8181
<li [ngClass]="{'navbar-tab-active': getActiveTabName() === 'settings', 'oppia-disabled-tab': !connectedToInternet}" class="nav-item icon nav-list-item e2e-test-settings-tab"
8282
(click)="!connectedToInternet || selectSettingsTab()" [ngbTooltip]="'Settings does not work when offline.'" [disableTooltip]="connectedToInternet" placement="bottom">
83-
<a class="nav-link navbar-tab"
83+
<a class="nav-link navbar-tab"
8484
[ngbTooltip]="'Settings'"
8585
placement="bottom"
8686
tabindex="0"

core/templates/pages/exploration-editor-page/settings-tab/settings-tab.component.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ <h3 tabindex="0">Roles</h3>
377377
<div class="oppia-roles-container">
378378
<div class="oppia-basic-settings-header oppia-mobile-collapsible-card-header" (click)="toggleCards('voice_artists')">
379379
<h3 tabindex="0">Voice Artists</h3>
380-
<i class="fa fa-caret-down"
380+
<i class="fa fa-caret-down e2e-test-voice-artist-collapsible-card-header"
381381
*ngIf="!voiceArtistsCardIsShown"
382382
aria-hidden="true">
383383
</i>
@@ -386,7 +386,7 @@ <h3 tabindex="0">Voice Artists</h3>
386386
aria-hidden="true">
387387
</i>
388388
</div>
389-
<div class="oppia-mobile-collapsible-card-content" *ngIf="voiceArtistsCardIsShown">
389+
<div class="oppia-mobile-collapsible-card-content e2e-test-voice-artist-card" *ngIf="voiceArtistsCardIsShown">
390390
<div>
391391
<div [hidden]="isVoiceoverFormOpen" tabindex="0" class="oppia-no-voice-artist-message" *ngIf="!(explorationRightsService.voiceArtistNames && explorationRightsService.voiceArtistNames.length)">
392392
No voice artists are assigned to this exploration.
@@ -399,9 +399,9 @@ <h3 tabindex="0">Voice Artists</h3>
399399
</div>
400400

401401
<div [hidden]="!(explorationRightsService.voiceArtistNames && explorationRightsService.voiceArtistNames.length > 0)">
402-
<ul>
402+
<ul class="e2e-test-voiceArtist-list">
403403
<li *ngFor="let voiceArtistName of explorationRightsService.voiceArtistNames; let index = index">
404-
<div class="oppia-user-list-item .e2e-test-voiceArtist-role-names e2e-test-voice-artist-{{voiceArtistName}}">
404+
<div class="oppia-user-list-item e2e-test-voiceArtist-role-names e2e-test-voice-artist-{{voiceArtistName}}">
405405
<span tabindex="0">{{voiceArtistName}}</span>
406406
<span [hidden]="!(isVoiceoverFormOpen)" type="button" [ngbTooltip]="'Remove user'" class="far fa-times-circle" tabindex="0" aria-label="remove user" role="button" (keydown.enter)="removeVoiceArtist(voiceArtistName)" (click)="removeVoiceArtist(voiceArtistName)">
407407
</span>

core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/test-constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import path from 'path';
2020

2121
export default {
2222
URLs: {
23+
BaseURL: 'http://localhost:8181',
2324
About: 'http://localhost:8181/about',
2425
AboutFoundation: 'http://localhost:8181/about-foundation',
2526
AdminPage: 'http://localhost:8181/admin',
@@ -71,6 +72,7 @@ export default {
7172
BLOG_ADMIN: 'blog admin',
7273
BLOG_POST_EDITOR: 'blog post editor',
7374
QUESTION_ADMIN: 'question admin',
75+
VOICEOVER_ADMIN: 'voiceover admin',
7476
} as const,
7577
BlogRights: {
7678
BLOG_ADMIN: 'BLOG_ADMIN',

core/tests/puppeteer-acceptance-tests/puppeteer-testing-utilities/user-factory.ts

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
import {BlogAdminFactory, BlogAdmin} from '../user-utilities/blog-admin-utils';
3030
import {QuestionAdminFactory} from '../user-utilities/question-admin-utils';
3131
import {BlogPostEditorFactory} from '../user-utilities/blog-post-editor-utils';
32+
import {VoiceoverAdminFactory} from '../user-utilities/voiceover-admin-utils';
33+
import {ExplorationEditorFactory} from '../user-utilities/exploration-creator-utils';
3234
import testConstants from './test-constants';
3335

3436
const ROLES = testConstants.Roles;
@@ -42,6 +44,7 @@ const USER_ROLE_MAPPING = {
4244
[ROLES.BLOG_ADMIN]: BlogAdminFactory,
4345
[ROLES.BLOG_POST_EDITOR]: BlogPostEditorFactory,
4446
[ROLES.QUESTION_ADMIN]: QuestionAdminFactory,
47+
[ROLES.VOICEOVER_ADMIN]: VoiceoverAdminFactory,
4548
} as const;
4649

4750
/**
@@ -141,6 +144,7 @@ export class UserFactory {
141144
): Promise<LoggedInUser & MultipleRoleIntersection<TRoles>> {
142145
let user = UserFactory.composeUserWithRoles(BaseUserFactory(), [
143146
LoggedInUserFactory(),
147+
ExplorationEditorFactory(),
144148
]);
145149
await user.openBrowser();
146150
await user.signUpNewUser(username, email);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2024 The Oppia Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS-IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* @fileoverview Utility functions for voiceover admin page if voiceover admin
17+
* can add voiceover artist to an exploration
18+
*/
19+
20+
import {UserFactory} from '../../puppeteer-testing-utilities/user-factory';
21+
import {VoiceoverAdmin} from '../../user-utilities/voiceover-admin-utils';
22+
import {ExplorationEditor} from '../../user-utilities/exploration-creator-utils';
23+
import testConstants from '../../puppeteer-testing-utilities/test-constants';
24+
import {ConsoleReporter} from '../../puppeteer-testing-utilities/console-reporter';
25+
26+
const DEFAULT_SPEC_TIMEOUT = testConstants.DEFAULT_SPEC_TIMEOUT;
27+
const ROLES = testConstants.Roles;
28+
const invalidIdErrorToastMessage =
29+
'Sorry, we could not find the specified user.';
30+
31+
// The backend 400 error is a known consequence of adding an invalid user ID.
32+
// By ignoring it, we prevent noise in the test output and focus on other unexpected errors.
33+
//
34+
// The frontend toast message is directly asserted by test case, ensuring it is displayed correctly.
35+
36+
ConsoleReporter.setConsoleErrorsToIgnore([
37+
new RegExp(
38+
'http://localhost:8181/voice_artist_management_handler/exploration/.*Failed to load resource: the server responded with a status of 400'
39+
),
40+
new RegExp('Sorry, we could not find the specified user.'),
41+
]);
42+
43+
describe('Voiceover Admin', function () {
44+
let voiceoverAdmin: VoiceoverAdmin;
45+
let explorationEditor: ExplorationEditor;
46+
let explorationId: string | null;
47+
48+
beforeAll(async function () {
49+
voiceoverAdmin = await UserFactory.createNewUser(
50+
'voiceoverAdm',
51+
'voiceover_admin@example.com',
52+
[ROLES.VOICEOVER_ADMIN]
53+
);
54+
55+
explorationEditor = await UserFactory.createNewUser(
56+
'explorationEditor',
57+
'exploration_creator@example.com'
58+
);
59+
}, DEFAULT_SPEC_TIMEOUT);
60+
61+
it(
62+
'should be able to see error while adding an invalid user as a voiceover artist to an exploration',
63+
async function () {
64+
await explorationEditor.navigateToCreatorDashboardPage();
65+
await explorationEditor.navigateToExplorationEditorPage();
66+
await explorationEditor.dismissWelcomeModal();
67+
68+
await explorationEditor.createExplorationWithTitle('Exploration one');
69+
explorationId =
70+
await explorationEditor.publishExplorationWithTitle('Exploration one');
71+
72+
await voiceoverAdmin.navigateToExplorationEditor(explorationId);
73+
await voiceoverAdmin.dismissWelcomeModal();
74+
75+
await voiceoverAdmin.navigateToExplorationSettingsTab();
76+
77+
await voiceoverAdmin.expectVoiceoverArtistsListDoesNotContain(
78+
'invalidUserId'
79+
);
80+
await voiceoverAdmin.addVoiceoverArtistToExploration('invalidUserId');
81+
82+
await voiceoverAdmin.expectToSeeErrorToastMessage(
83+
invalidIdErrorToastMessage
84+
);
85+
await voiceoverAdmin.closeToastMessage();
86+
await voiceoverAdmin.verifyVoiceoverArtistStillOmitted('invalidUserId');
87+
},
88+
DEFAULT_SPEC_TIMEOUT
89+
);
90+
91+
it(
92+
'should be able to add regular user as voiceover artist to an exploration',
93+
async function () {
94+
await UserFactory.createNewUser(
95+
'voiceoverartist',
96+
'voiceoverartist@example.com'
97+
);
98+
await explorationEditor.navigateToCreatorDashboardPage();
99+
await explorationEditor.navigateToExplorationEditorPage();
100+
101+
await explorationEditor.createExplorationWithTitle('Exploration two');
102+
explorationId =
103+
await explorationEditor.publishExplorationWithTitle('Exploration two');
104+
105+
await voiceoverAdmin.navigateToExplorationEditor(explorationId);
106+
await voiceoverAdmin.navigateToExplorationSettingsTab();
107+
108+
await voiceoverAdmin.expectVoiceoverArtistsListDoesNotContain(
109+
'voiceoverartist'
110+
);
111+
await voiceoverAdmin.addVoiceoverArtistToExploration('voiceoverartist');
112+
113+
await voiceoverAdmin.expectVoiceoverArtistsListContains(
114+
'voiceoverartist'
115+
);
116+
},
117+
DEFAULT_SPEC_TIMEOUT
118+
);
119+
120+
afterAll(async function () {
121+
await UserFactory.closeAllBrowsers();
122+
});
123+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2024 The Oppia Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS-IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* @fileoverview Utility functions for exploration creator page if exploration creator
17+
*/
18+
19+
import {BaseUser} from '../puppeteer-testing-utilities/puppeteer-utils';
20+
import testConstants from '../puppeteer-testing-utilities/test-constants';
21+
22+
const creatorDashboardPage = testConstants.URLs.CreatorDashboard;
23+
24+
const createExplorationButton = 'button.e2e-test-create-new-exploration-button';
25+
const dismissWelcomeModalSelector = 'button.e2e-test-dismiss-welcome-modal';
26+
const textStateEditSelector = 'div.e2e-test-state-edit-content';
27+
const richTextAreaField = 'div.e2e-test-rte';
28+
const saveContentButton = 'button.e2e-test-save-state-content';
29+
const addInteractionButton = 'button.e2e-test-open-add-interaction-modal';
30+
const interactionEndExplorationInputButton =
31+
'div.e2e-test-interaction-tile-EndExploration';
32+
const saveInteractionButton = 'button.e2e-test-save-interaction';
33+
const saveChangesButton = 'button.e2e-test-save-changes';
34+
const saveDraftButton = 'button.e2e-test-save-draft-button';
35+
36+
const publishExplorationButton = 'button.e2e-test-publish-exploration';
37+
const explorationTitleInput = 'input.e2e-test-exploration-title-input-modal';
38+
const explorationGoalInput = 'input.e2e-test-exploration-objective-input-modal';
39+
const explorationCategoryDropdown =
40+
'mat-form-field.e2e-test-exploration-category-metadata-modal';
41+
const saveExplorationChangesButton = 'button.e2e-test-confirm-pre-publication';
42+
const explorationConfirmPublishButton = 'button.e2e-test-confirm-publish';
43+
const explorationIdElement = 'span.oppia-unique-progress-id';
44+
const closeShareModalButton = 'button.e2e-test-share-publish-close';
45+
46+
const mobileNavToggelbutton = '.e2e-test-mobile-options';
47+
const mobileChangesDropdown = '.e2e-test-mobile-changes-dropdown';
48+
const mobileSaveChangesButton =
49+
'button.e2e-test-save-changes-for-small-screens';
50+
const mobilePublishButton = 'button.e2e-test-mobile-publish-button';
51+
52+
export class ExplorationEditor extends BaseUser {
53+
/**
54+
* Function to navigate to creator dashboard page
55+
*/
56+
async navigateToCreatorDashboardPage(): Promise<void> {
57+
await this.page.goto(creatorDashboardPage);
58+
}
59+
60+
/**
61+
* Function to navigate to exploration editor
62+
*/
63+
async navigateToExplorationEditorPage(): Promise<void> {
64+
await this.clickOn(createExplorationButton);
65+
}
66+
67+
/**
68+
* Function to create exploration with title
69+
* @param explorationTitle - title of the exploration
70+
*/
71+
async createExplorationWithTitle(explorationTitle: string): Promise<void> {
72+
await this.page.waitForFunction('document.readyState === "complete"');
73+
await this.page.waitForSelector(textStateEditSelector, {
74+
visible: true,
75+
});
76+
await this.clickOn(textStateEditSelector);
77+
await this.page.waitForSelector(richTextAreaField, {visible: true});
78+
await this.type(richTextAreaField, `${explorationTitle}`);
79+
await this.clickOn(saveContentButton);
80+
81+
await this.clickOn(addInteractionButton);
82+
await this.clickOn(interactionEndExplorationInputButton);
83+
await this.clickOn(saveInteractionButton);
84+
await this.page.waitForSelector('.customize-interaction-body-container', {
85+
hidden: true,
86+
});
87+
88+
if (this.isViewportAtMobileWidth()) {
89+
await this.clickOn(mobileNavToggelbutton);
90+
await this.clickOn(mobileSaveChangesButton);
91+
} else {
92+
await this.page.waitForSelector(`${saveChangesButton}:not([disabled])`);
93+
await this.clickOn(saveChangesButton);
94+
}
95+
96+
await this.page.waitForFunction('document.readyState === "complete"');
97+
await this.page.waitForSelector(saveDraftButton, {visible: true});
98+
await this.clickOn(saveDraftButton);
99+
await this.page.waitForSelector(saveDraftButton, {hidden: true});
100+
await this.page.waitForFunction('document.readyState === "complete"');
101+
}
102+
103+
/**
104+
* Function to publish exploration
105+
*/
106+
async publishExplorationWithTitle(
107+
explorationTitle: string
108+
): Promise<string | null> {
109+
if (this.isViewportAtMobileWidth()) {
110+
await this.page.waitForSelector('.e2e-test-toast-message', {
111+
visible: true,
112+
});
113+
await this.page.waitForSelector('.e2e-test-toast-message', {
114+
hidden: true,
115+
});
116+
await this.clickOn(mobileChangesDropdown);
117+
await this.clickOn(mobilePublishButton);
118+
} else {
119+
await this.page.waitForSelector(
120+
`${publishExplorationButton}:not([disabled])`
121+
);
122+
await this.clickOn(publishExplorationButton);
123+
}
124+
await this.clickOn(explorationTitleInput);
125+
await this.type(explorationTitleInput, `${explorationTitle}`);
126+
await this.clickOn(explorationGoalInput);
127+
await this.type(explorationGoalInput, `${explorationTitle}`);
128+
await this.clickOn(explorationCategoryDropdown);
129+
await this.clickOn('Algebra');
130+
await this.clickOn(saveExplorationChangesButton);
131+
await this.clickOn(explorationConfirmPublishButton);
132+
await this.page.waitForSelector(explorationIdElement);
133+
const explorationIdUrl = await this.page.$eval(
134+
explorationIdElement,
135+
element => (element as HTMLElement).innerText
136+
);
137+
const explorationId = explorationIdUrl.replace(/^.*\/explore\//, '');
138+
139+
await this.clickOn(closeShareModalButton);
140+
return explorationId;
141+
}
142+
143+
/**
144+
* Function to dismiss welcome modal
145+
*/
146+
async dismissWelcomeModal(): Promise<void> {
147+
await this.page.waitForSelector(dismissWelcomeModalSelector, {
148+
visible: true,
149+
});
150+
await this.clickOn(dismissWelcomeModalSelector);
151+
await this.page.waitForSelector(dismissWelcomeModalSelector, {
152+
hidden: true,
153+
});
154+
}
155+
}
156+
157+
export let ExplorationEditorFactory = (): ExplorationEditor =>
158+
new ExplorationEditor();

0 commit comments

Comments
 (0)