MOBILE-2652 glossary: Edit attachments

main
Noel De Martin 2023-03-30 12:39:00 +02:00
parent 9391fe4122
commit d2d8a814f6
13 changed files with 220 additions and 42 deletions

View File

@ -71,5 +71,14 @@ gulp.task('watch', () => {
});
gulp.task('watch-behat', () => {
gulp.watch(['./src/**/*.feature', './src/**/*.png', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat'));
gulp.watch(
[
'./src/**/*.feature',
'./src/**/tests/behat/fixtures/**',
'./src/**/tests/behat/snapshots/**',
'./local_moodleappbehat',
],
{ interval: 500 },
gulp.parallel('behat')
);
});

View File

@ -44,27 +44,21 @@ class behat_app extends behat_app_helper {
],
];
protected $featurepath = '';
protected $windowsize = '360x720';
/**
* @BeforeScenario
*/
public function before_scenario(ScenarioScope $scope) {
if (!$scope->getFeature()->hasTag('app')) {
$feature = $scope->getFeature();
if (!$feature->hasTag('app')) {
return;
}
global $CFG;
$performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
if ($performanceLogs !== 'ALL') {
return;
}
// Enable DB Logging only for app tests with performance logs activated.
$this->getSession()->visit($this->get_app_url() . '/assets/env.json');
$this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
$this->featurepath = dirname($feature->getFile());
$this->configure_performance_logs();
}
/**
@ -89,6 +83,23 @@ class behat_app extends behat_app_helper {
$this->enter_site();
}
/**
* Configure performance logs.
*/
protected function configure_performance_logs() {
global $CFG;
$performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
if ($performanceLogs !== 'ALL') {
return;
}
// Enable DB Logging only for app tests with performance logs activated.
$this->getSession()->visit($this->get_app_url() . '/assets/env.json');
$this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
}
/**
* Check whether the current page is the login form.
*/
@ -778,6 +789,35 @@ class behat_app extends behat_app_helper {
}
}
/**
* Uploads a file to a file input, the file path should be relative to a fixtures folder next to the feature file.
* The ìnput locator can match a container with a file input inside, it doesn't have to be the input itself.
*
* @Given /^I upload "((?:[^"]|\\")+)" to (".+") in the app$/
* @param string $filename
* @param string $inputlocator
*/
public function i_upload_a_file_in_the_app(string $filename, string $inputlocator) {
$filepath = str_replace('/', DIRECTORY_SEPARATOR, "{$this->featurepath}/fixtures/$filename");
$inputlocator = $this->parse_element_locator($inputlocator);
$id = $this->spin(function() use ($inputlocator) {
$result = $this->runtime_js("getFileInputId($inputlocator)");
if (str_starts_with($result, 'ERROR')) {
throw new DriverException('Error finding input - ' . $result);
}
return $result;
});
$this->wait_for_pending_js();
$fileinput = $this ->getSession()->getPage()->findById($id);
$fileinput->attachFile($filepath);
}
/**
* Checks a field matches a certain value in the app.
*

View File

@ -33,7 +33,7 @@ async function main() {
: [];
if (!existsSync(pluginPath)) {
mkdirSync(pluginPath);
mkdirSync(pluginPath, { recursive: true });
} else {
// Empty directory, except the excluding list.
const excludeFromErase = [
@ -76,21 +76,29 @@ async function main() {
};
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
// Copy feature and snapshot files.
// Copy features, snapshots, and fixtures.
if (!excludeFeatures) {
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
copySync(projectPath('src'), behatTempFeaturesPath, { filter: shouldCopyFileOrDirectory });
const behatFeaturesPath = `${pluginPath}/tests/behat`;
if (!existsSync(behatFeaturesPath)) {
mkdirSync(behatFeaturesPath, {recursive: true});
mkdirSync(behatFeaturesPath, { recursive: true });
}
for await (const file of getDirectoryFiles(behatTempFeaturesPath)) {
const filePath = dirname(file);
const snapshotsIndex = file.indexOf('/tests/behat/snapshots/');
const fixturesIndex = file.indexOf('/tests/behat/fixtures/');
if (filePath.endsWith('/tests/behat/snapshots')) {
renameSync(file, behatFeaturesPath + '/snapshots/' + basename(file));
if (snapshotsIndex !== -1) {
moveFile(file, behatFeaturesPath + '/snapshots/' + file.slice(snapshotsIndex + 23));
continue;
}
if (fixturesIndex !== -1) {
moveFile(file, behatFeaturesPath + '/fixtures/' + file.slice(fixturesIndex + 22));
continue;
}
@ -103,7 +111,7 @@ async function main() {
const searchRegExp = /\//g;
const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core';
const featureFilename = prefix + '-' + basename(file);
renameSync(file, behatFeaturesPath + '/' + featureFilename);
moveFile(file, behatFeaturesPath + '/' + featureFilename);
}
rmSync(behatTempFeaturesPath, {recursive: true});
@ -115,7 +123,8 @@ function shouldCopyFileOrDirectory(path) {
return stats.isDirectory()
|| extname(path) === '.feature'
|| extname(path) === '.png';
|| path.includes('/tests/behat/snapshots')
|| path.includes('/tests/behat/fixtures');
}
function isExcluded(file, exclusions) {
@ -127,6 +136,16 @@ function fail(message) {
process.exit(1);
}
function moveFile(from, to) {
const targetDir = dirname(to);
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
renameSync(from, to);
}
function guessPluginPath() {
if (process.env.MOODLE_APP_BEHAT_PLUGIN_PATH) {
return process.env.MOODLE_APP_BEHAT_PLUGIN_PATH;

View File

@ -419,6 +419,7 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler {
* @inheritdoc
*/
async save(glossary: AddonModGlossaryGlossary): Promise<boolean> {
const originalData = this.page.data;
const data = this.page.data;
// Upload attachments first if any.
@ -428,6 +429,10 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler {
offlineAttachments = await this.storeAttachments(glossary, data.timecreated);
}
if (originalData.concept !== data.concept) {
await AddonModGlossaryHelper.deleteStoredFiles(glossary.id, originalData.concept, data.timecreated);
}
// Save entry data.
await this.updateOfflineEntry(glossary, offlineAttachments);
@ -653,8 +658,18 @@ class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler {
const options = this.getSaveOptions(glossary);
const definition = CoreTextUtils.formatHtmlLines(data.definition);
// Upload attachments, if any.
let attachmentsId: number | undefined = undefined;
if (data.attachments.length) {
attachmentsId = await this.uploadAttachments(glossary);
}
// Save entry data.
await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options);
await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options, attachmentsId);
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(data.attachments);
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });

View File

@ -73,6 +73,10 @@
[componentId]="componentId">
</core-file>
</div>
<div *ngIf="offlineEntry && offlineEntryFiles">
<core-local-file *ngFor="let file of offlineEntryFiles" [file]="file">
</core-local-file>
</div>
<ion-item class="ion-text-wrap" *ngIf="onlineEntry && tagsEnabled && entry && onlineEntry.tags && onlineEntry.tags.length > 0">
<ion-label>
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>

View File

@ -23,6 +23,7 @@ import { CoreCommentsCommentsComponent } from '@features/comments/components/com
import { CoreComments } from '@features/comments/services/comments';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreTag } from '@features/tag/services/tag';
import { FileEntry } from '@ionic-native/file/ngx';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreNetwork } from '@services/network';
@ -54,6 +55,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
componentId?: number;
onlineEntry?: AddonModGlossaryEntry;
offlineEntry?: AddonModGlossaryOfflineEntry;
offlineEntryFiles?: FileEntry[];
entries!: AddonModGlossaryEntryEntriesSwipeManager;
glossary?: AddonModGlossaryGlossary;
entryUpdatedObserver?: CoreEventObserver;
@ -263,6 +265,13 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
const glossary = await this.loadGlossary();
this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated);
this.offlineEntryFiles = this.offlineEntry.attachments && this.offlineEntry.attachments.offline > 0
? await AddonModGlossaryHelper.getStoredFiles(
glossary.id,
this.offlineEntry.concept,
timecreated,
)
: undefined;
this.canEdit = true;
this.canDelete = true;
} catch (error) {

View File

@ -930,6 +930,7 @@ export class AddonModGlossaryProvider {
* @param concept Glossary entry concept.
* @param definition Glossary entry concept definition.
* @param options Options for the entry.
* @param attachId Attachments ID (if any attachment).
* @param siteId Site ID. If not defined, current site.
*/
async updateEntry(
@ -938,6 +939,7 @@ export class AddonModGlossaryProvider {
concept: string,
definition: string,
options?: Record<string, AddonModGlossaryEntryOption>,
attachId?: number,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
@ -950,6 +952,13 @@ export class AddonModGlossaryProvider {
options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'),
};
if (attachId) {
params.options?.push({
name: 'attachmentsid',
value: String(attachId),
});
}
const response = await site.write<AddonModGlossaryUpdateEntryWSResponse>('mod_glossary_update_entry', params);
if (!response.result) {

View File

@ -154,42 +154,51 @@ Feature: Test basic usage of glossary in app
Then I should find "Garlic" in the app
And I should find "Allium sativum" in the app
Scenario: Edit entries (basic info)
Scenario: Edit entries
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
# Online
When I press "Add a new entry" in the app
And I set the following fields to these values in the app:
| Concept | Cashew |
| Definition | Cashew is a fruit |
And I press "Save" in the app
Then I should find "Cashew" in the app
When I press "Cashew" in the app
When I press "Cucumber" in the app
And I press "Edit entry" in the app
Then the field "Concept" matches value "Cashew" in the app
And the field "Definition" matches value "Cashew is a fruit" in the app
Then the field "Concept" matches value "Cucumber" in the app
And the field "Definition" matches value "Sweet cucumber" in the app
When I set the following fields to these values in the app:
| Concept | Coconut |
| Definition | Coconut is a fruit |
And I press "Add file" in the app
And I upload "stub1.txt" to "File" ".action-sheet-button" in the app
And I press "Add file" in the app
And I upload "stub2.txt" to "File" ".action-sheet-button" in the app
And I press "This entry should be automatically linked" "ion-toggle" in the app
And I press "This entry is case sensitive" "ion-toggle" in the app
And I press "Match whole words only" "ion-toggle" in the app
And I press "Save" in the app
Then I should find "Coconut is a fruit" in the app
But I should not find "Cashew is a fruit" in the app
And I should find "stub1.txt" in the app
And I should find "stub2.txt" in the app
But I should not find "Cucumber is a fruit" in the app
When I press "Edit entry" in the app
Then "This entry should be automatically linked" "ion-toggle" should be selected in the app
Then I should find "stub1.txt" in the app
And I should find "stub2.txt" in the app
And "This entry should be automatically linked" "ion-toggle" should be selected in the app
And "This entry is case sensitive" "ion-toggle" should be selected in the app
And "Match whole words only" "ion-toggle" should be selected in the app
When I press "Save" in the app
And I press the back button in the app
When I press "Delete" within "stub2.txt" "ion-item" in the app
And I press "Delete" near "Are you sure you want to delete this file?" in the app
And I press "Add file" in the app
And I upload "stub3.txt" to "File" ".action-sheet-button" in the app
And I press "Save" in the app
Then I should find "stub1.txt" in the app
And I should find "stub3.txt" in the app
But I should not find "stub2.txt" in the app
When I press the back button in the app
Then I should find "Coconut" in the app
And I should find "Potato" in the app
But I should not find "Cashew" in the app
But I should not find "Cucumber" in the app
# Offline
When I press "Add a new entry" in the app
@ -197,6 +206,10 @@ Feature: Test basic usage of glossary in app
And I set the following fields to these values in the app:
| Concept | Broccoli |
| Definition | Brassica oleracea var. italica |
And I press "Add file" in the app
And I upload "stub1.txt" to "File" ".action-sheet-button" in the app
And I press "Add file" in the app
And I upload "stub2.txt" to "File" ".action-sheet-button" in the app
And I press "This entry should be automatically linked" "ion-toggle" in the app
And I press "This entry is case sensitive" "ion-toggle" in the app
And I press "Match whole words only" "ion-toggle" in the app
@ -206,10 +219,14 @@ Feature: Test basic usage of glossary in app
When I press "Broccoli" in the app
Then I should find "Brassica oleracea var. italica" in the app
And I should find "stub1.txt" in the app
And I should find "stub2.txt" in the app
When I press "Edit entry" in the app
Then the field "Concept" matches value "Broccoli" in the app
And the field "Definition" matches value "Brassica oleracea var. italica" in the app
And I should find "stub1.txt" in the app
And I should find "stub2.txt" in the app
And "This entry should be automatically linked" "ion-toggle" should be selected in the app
And "This entry is case sensitive" "ion-toggle" should be selected in the app
And "Match whole words only" "ion-toggle" should be selected in the app
@ -217,15 +234,34 @@ Feature: Test basic usage of glossary in app
When I set the following fields to these values in the app:
| Concept | Pickle |
| Definition | Pickle Rick |
And I press "Delete" within "stub2.txt" "ion-item" in the app
And I press "Delete" near "Are you sure you want to delete this file?" in the app
And I press "Add file" in the app
And I upload "stub3.txt" to "File" ".action-sheet-button" in the app
And I press "Save" in the app
Then I should find "Pickle Rick" in the app
But I should not find "Brassica oleracea var. italica" in the app
And I should find "stub1.txt" in the app
And I should find "stub3.txt" in the app
But I should not find "stub2.txt" in the app
And I should not find "Brassica oleracea var. italica" in the app
When I press the back button in the app
Then I should find "Pickle" in the app
And I should find "Potato" in the app
But I should not find "Broccoli" in the app
When I switch network connection to wifi
And I press "Information" in the app
And I press "Synchronise now" in the app
Then I should not find "This Glossary has offline data to be synchronised" in the app
When I press "Pickle" in the app
Then I should find "Pickle Rick" in the app
And I should find "stub1.txt" in the app
And I should find "stub3.txt" in the app
But I should not find "stub2.txt" in the app
And I should not find "Brassica oleracea var. italica" in the app
Scenario: Delete entries
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app

View File

@ -0,0 +1 @@
This is a stub.

View File

@ -0,0 +1 @@
This is a stub.

View File

@ -0,0 +1 @@
This is a stub.

View File

@ -421,7 +421,7 @@ export class TestingBehatDomUtilsService {
*/
findElementBasedOnText(
locator: TestingBehatElementLocator,
options: TestingBehatFindOptions,
options: TestingBehatFindOptions = {},
): HTMLElement | undefined {
return this.findElementsBasedOnText(locator, options)[0];
}
@ -437,7 +437,7 @@ export class TestingBehatDomUtilsService {
locator: TestingBehatElementLocator,
options: TestingBehatFindOptions,
): HTMLElement[] {
const topContainers = this.getCurrentTopContainerElements(options.containerName);
const topContainers = this.getCurrentTopContainerElements(options.containerName ?? '');
let elements: HTMLElement[] = [];
for (let i = 0; i < topContainers.length; i++) {

View File

@ -361,6 +361,40 @@ export class TestingBehatRuntimeService {
}
}
/**
* Get a file input id, adding it if necessary.
*
* @param locator Input locator.
* @returns Input id if successful, or ERROR: followed by message
*/
async getFileInputId(locator: TestingBehatElementLocator): Promise<string> {
this.log('Action - Upload File', { locator });
try {
const inputOrContainer = TestingBehatDomUtils.findElementBasedOnText(locator);
if (!inputOrContainer) {
return 'ERROR: No element matches input locator.';
}
const input = inputOrContainer.matches('input[type="file"]')
? inputOrContainer
: inputOrContainer.querySelector('input[type="file"]');
if (!input) {
return 'ERROR: Input element does not contain a file input.';
}
if (!input.hasAttribute('id')) {
input.setAttribute('id', `file-${Date.now()}`);
}
return input.getAttribute('id') ?? '';
} catch (error) {
return 'ERROR: ' + error.message;
}
}
/**
* Trigger a pull to refresh gesture in the current page.
*
@ -635,8 +669,8 @@ export type BehatTestsWindow = Window & {
};
export type TestingBehatFindOptions = {
containerName: string;
onlyClickable: boolean;
containerName?: string;
onlyClickable?: boolean;
};
export type TestingBehatElementLocator = {