From 38d0ad1aaddba5c133d27e5733633bb3d857a1f8 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 10:43:04 +0100 Subject: [PATCH 01/19] MOBILE-4304 core: Remove redundant generics --- src/core/features/course/services/course.ts | 2 +- .../pushnotifications/services/pushnotifications.ts | 2 +- src/core/services/filepool.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 7f05c23ae8d..2f04919164c 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -137,7 +137,7 @@ export class CoreCourseProvider { this.viewedModulesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(COURSE_VIEWED_MODULES_TABLE, { + () => CoreSites.getSiteTable(COURSE_VIEWED_MODULES_TABLE, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, primaryKeyColumns: ['courseId', 'cmId'], diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index 4413f8d33f1..bfcb5dbca09 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -72,7 +72,7 @@ export class CorePushNotificationsProvider { this.logger = CoreLogger.getInstance('CorePushNotificationsProvider'); this.registeredDevicesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable( + () => CoreSites.getSiteTable( REGISTERED_DEVICES_TABLE_NAME, { siteId, diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 8303fb85dd9..f4f1fc7644c 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -113,7 +113,7 @@ export class CoreFilepoolProvider { this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); this.filesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(FILES_TABLE_NAME, { + () => CoreSites.getSiteTable(FILES_TABLE_NAME, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, primaryKeyColumns: ['fileId'], @@ -123,7 +123,7 @@ export class CoreFilepoolProvider { ); this.linksTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(LINKS_TABLE_NAME, { + () => CoreSites.getSiteTable(LINKS_TABLE_NAME, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, primaryKeyColumns: ['fileId', 'component', 'componentId'], @@ -133,7 +133,7 @@ export class CoreFilepoolProvider { ); this.packagesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(PACKAGES_TABLE_NAME, { + () => CoreSites.getSiteTable(PACKAGES_TABLE_NAME, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, onDestroy: () => delete this.packagesTables[siteId], From b6f32dfddd121945de297bbb2ad06d47738d94d4 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 10:44:24 +0100 Subject: [PATCH 02/19] MOBILE-4304 scorm: Update database usage --- .../mod/scorm/services/scorm-offline.ts | 254 +++++++++--------- src/core/classes/database/database-table.ts | 12 + 2 files changed, 140 insertions(+), 126 deletions(-) diff --git a/src/addons/mod/scorm/services/scorm-offline.ts b/src/addons/mod/scorm/services/scorm-offline.ts index 11af7f0450f..343da11c500 100644 --- a/src/addons/mod/scorm/services/scorm-offline.ts +++ b/src/addons/mod/scorm/services/scorm-offline.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreUser } from '@features/user/services/user'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; @@ -38,6 +37,10 @@ import { AddonModScormUserDataMap, AddonModScormWSSco, } from './scorm'; +import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; +import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Service to handle offline SCORM. @@ -47,8 +50,36 @@ export class AddonModScormOfflineProvider { protected logger: CoreLogger; + protected tracksTables: LazyMap< + AsyncInstance> + >; + + protected attemptsTables: LazyMap< + AsyncInstance> + >; + constructor() { this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider'); + this.tracksTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(TRACKS_TABLE_NAME, { + siteId, + primaryKeyColumns: ['scormid', 'userid', 'attempt', 'scoid', 'element'], + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.tracksTables[siteId], + }), + ), + ); + this.attemptsTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(ATTEMPTS_TABLE_NAME, { + siteId, + primaryKeyColumns: ['scormid', 'userid', 'attempt'], + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.tracksTables[siteId], + }), + ), + ); } /** @@ -76,40 +107,43 @@ export class AddonModScormOfflineProvider { this.logger.debug(`Change attempt number from ${attempt} to ${newAttempt} in SCORM ${scormId}`); - // Update the attempt number. - const db = site.getDb(); - const currentAttemptConditions: AddonModScormOfflineDBCommonData = { - scormid: scormId, - userid: userId, - attempt, - }; - const newAttemptConditions: AddonModScormOfflineDBCommonData = { - scormid: scormId, - userid: userId, - attempt: newAttempt, - }; - const newAttemptData: Partial = { - attempt: newAttempt, - timemodified: CoreTimeUtils.timestamp(), - }; - // Block the SCORM so it can't be synced. CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scormId, 'changeAttemptNumber', site.id); try { - await db.updateRecords(ATTEMPTS_TABLE_NAME, newAttemptData, currentAttemptConditions); + const currentAttemptConditions = { + sql: 'scormid = ? AND userid = ? AND attempt = ?', + sqlParams: [scormId, userId, attempt], + js: (record: AddonModScormOfflineDBCommonData) => + record.scormid === scormId && + record.userid === userId && + record.attempt === attempt, + }; + + await this.attemptsTables[site.id].updateWhere( + { attempt: newAttempt, timemodified: CoreTimeUtils.timestamp() }, + currentAttemptConditions, + ); try { // Now update the attempt number of all the tracks and mark them as not synced. - const newTrackData: Partial = { - attempt: newAttempt, - synced: 0, - }; - - await db.updateRecords(TRACKS_TABLE_NAME, newTrackData, currentAttemptConditions); + await this.tracksTables[site.id].updateWhere( + { attempt: newAttempt, synced: 0 }, + currentAttemptConditions, + ); } catch (error) { // Failed to update the tracks, restore the old attempt number. - await db.updateRecords(ATTEMPTS_TABLE_NAME, { attempt }, newAttemptConditions); + await this.attemptsTables[site.id].updateWhere( + { attempt }, + { + sql: 'scormid = ? AND userid = ? AND attempt = ?', + sqlParams: [scormId, userId, newAttempt], + js: (attempt) => + attempt.scormid === scormId && + attempt.userid === userId && + attempt.attempt === newAttempt, + }, + ); throw error; } @@ -148,7 +182,6 @@ export class AddonModScormOfflineProvider { CoreSync.blockOperation(AddonModScormProvider.COMPONENT, scorm.id, 'createNewAttempt', site.id); // Create attempt in DB. - const db = site.getDb(); const entry: AddonModScormAttemptDBRecord = { scormid: scorm.id, userid: userId, @@ -166,7 +199,7 @@ export class AddonModScormOfflineProvider { } try { - await db.insertRecord(ATTEMPTS_TABLE_NAME, entry); + await this.attemptsTables[site.id].insert(entry); // Store all the data in userData. const promises: Promise[] = []; @@ -204,16 +237,15 @@ export class AddonModScormOfflineProvider { this.logger.debug(`Delete offline attempt ${attempt} in SCORM ${scormId}`); - const db = site.getDb(); - const conditions: AddonModScormOfflineDBCommonData = { + const conditions = { scormid: scormId, userid: userId, attempt, }; await Promise.all([ - db.deleteRecords(ATTEMPTS_TABLE_NAME, conditions), - db.deleteRecords(TRACKS_TABLE_NAME, conditions), + this.attemptsTables[site.id].delete(conditions), + this.tracksTables[site.id].delete(conditions), ]); } @@ -280,9 +312,9 @@ export class AddonModScormOfflineProvider { * @returns Promise resolved when the offline attempts are retrieved. */ async getAllAttempts(siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - const attempts = await db.getAllRecords(ATTEMPTS_TABLE_NAME); + const attempts = await this.attemptsTables[siteId].getMany(); return attempts.map((attempt) => this.parseAttempt(attempt)); } @@ -300,7 +332,7 @@ export class AddonModScormOfflineProvider { const site = await CoreSites.getSite(siteId); userId = userId || site.getUserId(); - const attemptRecord = await site.getDb().getRecord(ATTEMPTS_TABLE_NAME, { + const attemptRecord = await this.attemptsTables[site.id].getOneByPrimaryKey({ scormid: scormId, userid: userId, attempt, @@ -340,7 +372,7 @@ export class AddonModScormOfflineProvider { const site = await CoreSites.getSite(siteId); userId = userId || site.getUserId(); - const attempts = await site.getDb().getRecords(ATTEMPTS_TABLE_NAME, { + const attempts = await this.attemptsTables[site.id].getMany({ scormid: scormId, userid: userId, }); @@ -428,7 +460,7 @@ export class AddonModScormOfflineProvider { conditions.synced = 1; } - const tracks = await site.getDb().getRecords(TRACKS_TABLE_NAME, conditions); + const tracks = await this.tracksTables[site.id].getMany(conditions); return this.parseTracks(tracks); } @@ -598,7 +630,6 @@ export class AddonModScormOfflineProvider { userId = userId || site.getUserId(); const scoUserData = scoData?.userdata || {}; - const db = site.getDb(); let lessonStatusInserted = false; if (forceCompleted) { @@ -611,7 +642,16 @@ export class AddonModScormOfflineProvider { if (scoUserData['cmi.core.lesson_status'] == 'incomplete') { lessonStatusInserted = true; - await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed'); + await this.tracksTables[site.id].insert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: 'cmi.core.lesson_status', + value: JSON.stringify('completed'), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } } } @@ -622,81 +662,35 @@ export class AddonModScormOfflineProvider { } try { - await this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value); + await this.tracksTables[site.id].insert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element, + value: value === undefined ? null : JSON.stringify(value), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } catch (error) { if (lessonStatusInserted) { // Rollback previous insert. - await this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete'); + await this.tracksTables[site.id].insert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: 'cmi.core.lesson_status', + value: JSON.stringify('incomplete'), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } throw error; } } - /** - * Insert a track in the DB. - * - * @param db Site's DB. - * @param userId User ID. - * @param scormId SCORM ID. - * @param scoId SCO ID. - * @param attempt Attempt number. - * @param element Name of the element to insert. - * @param value Value of the element to insert. - * @param synchronous True if insert should NOT return a promise. Please use it only if synchronous is a must. - * @returns Returns a promise if synchronous=false, otherwise returns a boolean. - */ - protected insertTrackToDB( - db: SQLiteDB, - userId: number, - scormId: number, - scoId: number, - attempt: number, - element: string, - value: AddonModScormDataValue | undefined, - synchronous: true, - ): boolean; - protected insertTrackToDB( - db: SQLiteDB, - userId: number, - scormId: number, - scoId: number, - attempt: number, - element: string, - value?: AddonModScormDataValue, - synchronous?: false, - ): Promise; - protected insertTrackToDB( - db: SQLiteDB, - userId: number, - scormId: number, - scoId: number, - attempt: number, - element: string, - value?: AddonModScormDataValue, - synchronous?: boolean, - ): boolean | Promise { - const entry: AddonModScormTrackDBRecord = { - userid: userId, - scormid: scormId, - scoid: scoId, - attempt, - element: element, - value: value === undefined ? null : JSON.stringify(value), - timemodified: CoreTimeUtils.timestamp(), - synced: 0, - }; - - if (synchronous) { - // The insert operation is always asynchronous, always return true. - db.insertRecord(TRACKS_TABLE_NAME, entry); - - return true; - } else { - return db.insertRecord(TRACKS_TABLE_NAME, entry); - } - } - /** * Insert a track in the offline tracks store, returning a synchronous value. * Please use this function only if synchronous is a must. It's recommended to use insertTrack. @@ -730,8 +724,7 @@ export class AddonModScormOfflineProvider { } const scoUserData = scoData?.userdata || {}; - const db = CoreSites.getRequiredCurrentSite().getDb(); - let lessonStatusInserted = false; + const siteId = CoreSites.getRequiredCurrentSite().id; if (forceCompleted) { if (element == 'cmi.core.lesson_status' && value == 'incomplete') { @@ -741,11 +734,16 @@ export class AddonModScormOfflineProvider { } if (element == 'cmi.core.score.raw') { if (scoUserData['cmi.core.lesson_status'] == 'incomplete') { - lessonStatusInserted = true; - - if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'completed', true)) { - return false; - } + this.tracksTables[siteId].syncInsert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: 'cmi.core.lesson_status', + value: JSON.stringify('completed'), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); } } } @@ -755,15 +753,16 @@ export class AddonModScormOfflineProvider { return true; } - if (!this.insertTrackToDB(db, userId, scormId, scoId, attempt, element, value, true)) { - // Insert failed. - if (lessonStatusInserted) { - // Rollback previous insert. - this.insertTrackToDB(db, userId, scormId, scoId, attempt, 'cmi.core.lesson_status', 'incomplete', true); - } - - return false; - } + this.tracksTables[siteId].syncInsert({ + userid: userId, + scormid: scormId, + scoid: scoId, + attempt, + element: element, + value: value === undefined ? null : JSON.stringify(value), + timemodified: CoreTimeUtils.timestamp(), + synced: 0, + }); return true; } @@ -784,7 +783,7 @@ export class AddonModScormOfflineProvider { this.logger.debug(`Mark SCO ${scoId} as synced for attempt ${attempt} in SCORM ${scormId}`); - await site.getDb().updateRecords(TRACKS_TABLE_NAME, { synced: 1 }, > { + await this.tracksTables[site.id].update({ synced: 1 }, { scormid: scormId, userid: userId, attempt, @@ -971,10 +970,13 @@ export class AddonModScormOfflineProvider { snapshot: JSON.stringify(this.removeDefaultData(userData)), }; - await site.getDb().updateRecords(ATTEMPTS_TABLE_NAME, newData, > { - scormid: scormId, - userid: userId, - attempt, + await this.attemptsTables[site.id].updateWhere(newData, { + sql: 'scormid = ? AND userid = ? AND attempt = ?', + sqlParams: [scormId, userId, attempt], + js: (record: AddonModScormOfflineDBCommonData) => + record.scormid === scormId && + record.userid === userId && + record.attempt === attempt, }); } diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index dc93ec1bc24..f78d287562b 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -258,6 +258,18 @@ export class CoreDatabaseTable< await this.database.insertRecord(this.tableName, record); } + /** + * Insert a new record synchronously. + * + * @param record Database record. + */ + syncInsert(record: DBRecord): void { + // The current database architecture does not support synchronous operations, + // so calling this method will mean that errors will be silenced. Because of that, + // this should only be called if using the asynchronous alternatives is not possible. + this.insert(record); + } + /** * Update records matching the given conditions. * From 869f08eee7d1b8ddab0ea70bc191cfd1f09fe56c Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 12:19:18 +0100 Subject: [PATCH 03/19] MOBILE-4304 core: Return database records rowId --- .../mod/scorm/services/database/scorm.ts | 10 +++- .../mod/scorm/services/scorm-offline.ts | 40 ++++++++++------ .../classes/database/database-table-proxy.ts | 15 +++--- src/core/classes/database/database-table.ts | 23 +++++++--- .../classes/database/debug-database-table.ts | 13 +++--- .../classes/database/eager-database-table.ts | 13 +++--- .../database/inmemory-database-table.ts | 29 +++++++++++- .../classes/database/lazy-database-table.ts | 11 +++-- src/core/classes/sites/site.ts | 11 +++-- src/core/features/course/services/course.ts | 27 +++++++---- .../course/services/database/course.ts | 5 +- .../services/database/pushnotifications.ts | 10 +++- .../services/pushnotifications.ts | 31 ++++++++++--- .../features/usertours/services/user-tours.ts | 1 - src/core/services/database/filepool.ts | 16 +++++-- src/core/services/database/sites.ts | 5 +- src/core/services/filepool.ts | 46 +++++++++++-------- src/core/services/sites.ts | 14 ++++-- 18 files changed, 219 insertions(+), 101 deletions(-) diff --git a/src/addons/mod/scorm/services/database/scorm.ts b/src/addons/mod/scorm/services/database/scorm.ts index e6e356fe62c..39830b2e4cf 100644 --- a/src/addons/mod/scorm/services/database/scorm.ts +++ b/src/addons/mod/scorm/services/database/scorm.ts @@ -18,7 +18,9 @@ import { CoreSiteSchema } from '@services/sites'; * Database variables for AddonModScormOfflineProvider. */ export const ATTEMPTS_TABLE_NAME = 'addon_mod_scorm_offline_attempts'; +export const ATTEMPTS_TABLE_PRIMARY_KEYS = ['scormid', 'userid', 'attempt'] as const; export const TRACKS_TABLE_NAME = 'addon_mod_scorm_offline_scos_tracks'; +export const TRACKS_TABLE_PRIMARY_KEYS = ['scormid', 'userid', 'attempt', 'scoid', 'element'] as const; export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { name: 'AddonModScormOfflineProvider', version: 1, @@ -58,7 +60,7 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { type: 'TEXT', }, ], - primaryKeys: ['scormid', 'userid', 'attempt'], + primaryKeys: [...ATTEMPTS_TABLE_PRIMARY_KEYS], }, { name: TRACKS_TABLE_NAME, @@ -101,7 +103,7 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { type: 'INTEGER', }, ], - primaryKeys: ['scormid', 'userid', 'attempt', 'scoid', 'element'], + primaryKeys: [...TRACKS_TABLE_PRIMARY_KEYS], }, ], }; @@ -125,6 +127,8 @@ export type AddonModScormAttemptDBRecord = AddonModScormOfflineDBCommonData & { snapshot?: string | null; }; +export type AddonModScormAttemptDBPrimaryKeys = typeof ATTEMPTS_TABLE_PRIMARY_KEYS[number]; + /** * SCORM track data. */ @@ -135,3 +139,5 @@ export type AddonModScormTrackDBRecord = AddonModScormOfflineDBCommonData & { timemodified: number; synced: number; }; + +export type AddonModScormTrackDBPrimaryKeys = typeof TRACKS_TABLE_PRIMARY_KEYS[number]; diff --git a/src/addons/mod/scorm/services/scorm-offline.ts b/src/addons/mod/scorm/services/scorm-offline.ts index 343da11c500..907b103ce07 100644 --- a/src/addons/mod/scorm/services/scorm-offline.ts +++ b/src/addons/mod/scorm/services/scorm-offline.ts @@ -22,11 +22,15 @@ import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { + AddonModScormAttemptDBPrimaryKeys, AddonModScormAttemptDBRecord, AddonModScormOfflineDBCommonData, + AddonModScormTrackDBPrimaryKeys, AddonModScormTrackDBRecord, ATTEMPTS_TABLE_NAME, + ATTEMPTS_TABLE_PRIMARY_KEYS, TRACKS_TABLE_NAME, + TRACKS_TABLE_PRIMARY_KEYS, } from './database/scorm'; import { AddonModScormDataEntry, @@ -51,33 +55,41 @@ export class AddonModScormOfflineProvider { protected logger: CoreLogger; protected tracksTables: LazyMap< - AsyncInstance> + AsyncInstance> >; protected attemptsTables: LazyMap< - AsyncInstance> + AsyncInstance> >; constructor() { this.logger = CoreLogger.getInstance('AddonModScormOfflineProvider'); this.tracksTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(TRACKS_TABLE_NAME, { - siteId, - primaryKeyColumns: ['scormid', 'userid', 'attempt', 'scoid', 'element'], - config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, - onDestroy: () => delete this.tracksTables[siteId], - }), + () => CoreSites.getSiteTable( + TRACKS_TABLE_NAME, + { + siteId, + primaryKeyColumns: [...TRACKS_TABLE_PRIMARY_KEYS], + rowIdColumn: null, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.tracksTables[siteId], + }, + ), ), ); this.attemptsTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(ATTEMPTS_TABLE_NAME, { - siteId, - primaryKeyColumns: ['scormid', 'userid', 'attempt'], - config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, - onDestroy: () => delete this.tracksTables[siteId], - }), + () => CoreSites.getSiteTable( + ATTEMPTS_TABLE_NAME, + { + siteId, + primaryKeyColumns: [...ATTEMPTS_TABLE_PRIMARY_KEYS], + rowIdColumn: null, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.tracksTables[siteId], + }, + ), ), ); } diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index ea7bffd965a..4bf1ede74ca 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -38,16 +38,17 @@ import { CoreLazyDatabaseTable } from './lazy-database-table'; export class CoreDatabaseTableProxy< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey -> extends CoreDatabaseTable { + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, +> extends CoreDatabaseTable { protected readonly DEFAULT_CACHING_STRATEGY = CoreDatabaseCachingStrategy.None; - protected target = asyncInstance>(); + protected target = asyncInstance>(); protected environmentObserver?: CoreEventObserver; protected targetConstructors: Record< CoreDatabaseCachingStrategy, - CoreDatabaseTableConstructor + CoreDatabaseTableConstructor > = { [CoreDatabaseCachingStrategy.Eager]: CoreEagerDatabaseTable, [CoreDatabaseCachingStrategy.Lazy]: CoreLazyDatabaseTable, @@ -154,7 +155,7 @@ export class CoreDatabaseTableProxy< /** * @inheritdoc */ - async insert(record: DBRecord): Promise { + async insert(record: Omit & Partial>): Promise { return this.target.insert(record); } @@ -239,7 +240,7 @@ export class CoreDatabaseTableProxy< * * @returns Target instance. */ - protected async createTarget(): Promise> { + protected async createTarget(): Promise> { const config = await this.getRuntimeConfig(); const table = this.createTable(config); @@ -252,7 +253,7 @@ export class CoreDatabaseTableProxy< * @param config Database configuration. * @returns Database table. */ - protected createTable(config: Partial): CoreDatabaseTable { + protected createTable(config: Partial): CoreDatabaseTable { const DatabaseTable = this.targetConstructors[config.cachingStrategy ?? this.DEFAULT_CACHING_STRATEGY]; return new DatabaseTable(config, this.database, this.tableName, this.primaryKeyColumns); diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index f78d287562b..5cb2131fbe6 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -21,13 +21,15 @@ import { SQLiteDB, SQLiteDBRecordValue, SQLiteDBRecordValues } from '@classes/sq export class CoreDatabaseTable< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, > { protected config: Partial; protected database: SQLiteDB; protected tableName: string; protected primaryKeyColumns: PrimaryKeyColumn[]; + protected rowIdColumn: RowIdColumn | null; protected listeners: CoreDatabaseTableListener[] = []; constructor( @@ -35,11 +37,13 @@ export class CoreDatabaseTable< database: SQLiteDB, tableName: string, primaryKeyColumns?: PrimaryKeyColumn[], + rowIdColumn?: RowIdColumn | null, ) { this.config = config; this.database = database; this.tableName = tableName; this.primaryKeyColumns = primaryKeyColumns ?? ['id'] as PrimaryKeyColumn[]; + this.rowIdColumn = rowIdColumn === null ? null : (rowIdColumn ?? 'id') as RowIdColumn; } /** @@ -253,9 +257,12 @@ export class CoreDatabaseTable< * Insert a new record. * * @param record Database record. + * @returns New record row id. */ - async insert(record: DBRecord): Promise { - await this.database.insertRecord(this.tableName, record); + async insert(record: Omit & Partial>): Promise { + const rowId = await this.database.insertRecord(this.tableName, record); + + return rowId; } /** @@ -263,7 +270,7 @@ export class CoreDatabaseTable< * * @param record Database record. */ - syncInsert(record: DBRecord): void { + syncInsert(record: Omit & Partial>): void { // The current database architecture does not support synchronous operations, // so calling this method will mean that errors will be silenced. Because of that, // this should only be called if using the asynchronous alternatives is not possible. @@ -423,15 +430,17 @@ export interface CoreDatabaseTableListener { export type CoreDatabaseTableConstructor< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, > = { new ( config: Partial, database: SQLiteDB, tableName: string, - primaryKeyColumns?: PrimaryKeyColumn[] - ): CoreDatabaseTable; + primaryKeyColumns?: PrimaryKeyColumn[], + rowIdColumn?: RowIdColumn | null, + ): CoreDatabaseTable; }; diff --git a/src/core/classes/database/debug-database-table.ts b/src/core/classes/database/debug-database-table.ts index 1328a8eea33..c3906df25e1 100644 --- a/src/core/classes/database/debug-database-table.ts +++ b/src/core/classes/database/debug-database-table.ts @@ -30,13 +30,14 @@ import { export class CoreDebugDatabaseTable< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey -> extends CoreDatabaseTable { + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, +> extends CoreDatabaseTable { - protected target: CoreDatabaseTable; + protected target: CoreDatabaseTable; protected logger: CoreLogger; - constructor(target: CoreDatabaseTable) { + constructor(target: CoreDatabaseTable) { super(target.getConfig(), target.getDatabase(), target.getTableName(), target.getPrimaryKeyColumns()); this.target = target; @@ -48,7 +49,7 @@ export class CoreDebugDatabaseTable< * * @returns Table instance. */ - getTarget(): CoreDatabaseTable { + getTarget(): CoreDatabaseTable { return this.target; } @@ -152,7 +153,7 @@ export class CoreDebugDatabaseTable< /** * @inheritdoc */ - insert(record: DBRecord): Promise { + insert(record: Omit & Partial>): Promise { this.logger.log('insert', record); return this.target.insert(record); diff --git a/src/core/classes/database/eager-database-table.ts b/src/core/classes/database/eager-database-table.ts index 5db50316286..3ed457c509f 100644 --- a/src/core/classes/database/eager-database-table.ts +++ b/src/core/classes/database/eager-database-table.ts @@ -31,8 +31,9 @@ import { export class CoreEagerDatabaseTable< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey -> extends CoreInMemoryDatabaseTable { + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, +> extends CoreInMemoryDatabaseTable { protected records: Record = {}; @@ -153,12 +154,10 @@ export class CoreEagerDatabaseTable< /** * @inheritdoc */ - async insert(record: DBRecord): Promise { - await super.insert(record); + async insert(record: Omit & Partial>): Promise { + const rowId = await this.insertAndRemember(record, this.records); - const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); - - this.records[primaryKey] = record; + return rowId; } /** diff --git a/src/core/classes/database/inmemory-database-table.ts b/src/core/classes/database/inmemory-database-table.ts index 6637f1f5416..3af78a4708e 100644 --- a/src/core/classes/database/inmemory-database-table.ts +++ b/src/core/classes/database/inmemory-database-table.ts @@ -26,8 +26,9 @@ import { CoreDatabaseTable, GetDBRecordPrimaryKey } from './database-table'; export abstract class CoreInMemoryDatabaseTable< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey -> extends CoreDatabaseTable { + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, +> extends CoreDatabaseTable { private static readonly ACTIVE_TABLES: WeakMap> = new WeakMap(); private static readonly LOGGER: CoreLogger = CoreLogger.getInstance('CoreInMemoryDatabaseTable'); @@ -70,4 +71,28 @@ export abstract class CoreInMemoryDatabaseTable< } } + /** + * Insert a new record and store it in the given object. + * + * @param record Database record. + * @param records Records object. + * @returns New record row id. + */ + protected async insertAndRemember( + record: Omit & Partial>, + records: Record, + ): Promise { + const rowId = await super.insert(record); + + const completeRecord = (this.rowIdColumn && !(this.rowIdColumn in record)) + ? Object.assign({ [this.rowIdColumn]: rowId }, record) as DBRecord + : record as DBRecord; + + const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(completeRecord)); + + records[primaryKey] = completeRecord; + + return rowId; + } + } diff --git a/src/core/classes/database/lazy-database-table.ts b/src/core/classes/database/lazy-database-table.ts index faee315d0c5..c8dd76c3179 100644 --- a/src/core/classes/database/lazy-database-table.ts +++ b/src/core/classes/database/lazy-database-table.ts @@ -31,8 +31,9 @@ import { export class CoreLazyDatabaseTable< DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, PrimaryKeyColumn extends keyof DBRecord = 'id', - PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey -> extends CoreInMemoryDatabaseTable { + RowIdColumn extends PrimaryKeyColumn = PrimaryKeyColumn, + PrimaryKey extends GetDBRecordPrimaryKey = GetDBRecordPrimaryKey, +> extends CoreInMemoryDatabaseTable { protected readonly DEFAULT_CACHE_LIFETIME = 60000; @@ -137,10 +138,10 @@ export class CoreLazyDatabaseTable< /** * @inheritdoc */ - async insert(record: DBRecord): Promise { - await super.insert(record); + async insert(record: Omit & Partial>): Promise { + const rowId = await this.insertAndRemember(record, this.records); - this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record; + return rowId; } /** diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index b19afa53275..da6267c7c97 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -42,8 +42,10 @@ import { CoreDatabaseCachingStrategy } from '../database/database-table-proxy'; import { CONFIG_TABLE, CoreSiteConfigDBRecord, + CoreSiteLastViewedDBPrimaryKeys, CoreSiteLastViewedDBRecord, CoreSiteWSCacheRecord, + LAST_VIEWED_PRIMARY_KEYS, LAST_VIEWED_TABLE, WS_CACHE_TABLE, } from '@services/database/sites'; @@ -65,8 +67,8 @@ export class CoreSite extends CoreAuthenticatedSite { protected db!: SQLiteDB; protected cacheTable: AsyncInstance>; - protected configTable: AsyncInstance>; - protected lastViewedTable: AsyncInstance>; + protected configTable: AsyncInstance>; + protected lastViewedTable: AsyncInstance>; protected lastAutoLogin = 0; protected tokenPluginFileWorks?: boolean; protected tokenPluginFileWorksPromise?: Promise; @@ -99,18 +101,19 @@ export class CoreSite extends CoreAuthenticatedSite { config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, })); - this.configTable = asyncInstance(() => CoreSites.getSiteTable(CONFIG_TABLE, { + this.configTable = asyncInstance(() => CoreSites.getSiteTable(CONFIG_TABLE, { siteId: this.getId(), database: this.getDb(), config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, primaryKeyColumns: ['name'], + rowIdColumn: null, })); this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(LAST_VIEWED_TABLE, { siteId: this.getId(), database: this.getDb(), config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, - primaryKeyColumns: ['component', 'id'], + primaryKeyColumns: [...LAST_VIEWED_PRIMARY_KEYS], })); this.setInfo(otherData.info); this.calculateOfflineDisabled(); diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 2f04919164c..b2cb8d0c68d 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -27,7 +27,12 @@ import { makeSingleton, Translate } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { - CoreCourseStatusDBRecord, CoreCourseViewedModulesDBRecord, COURSE_STATUS_TABLE, COURSE_VIEWED_MODULES_TABLE , + CoreCourseStatusDBRecord, + CoreCourseViewedModulesDBPrimaryKeys, + CoreCourseViewedModulesDBRecord, + COURSE_STATUS_TABLE, + COURSE_VIEWED_MODULES_PRIMARY_KEYS, + COURSE_VIEWED_MODULES_TABLE, } from './database/course'; import { CoreCourseOffline } from './course-offline'; import { CoreError } from '@classes/errors/error'; @@ -121,7 +126,9 @@ export class CoreCourseProvider { protected logger: CoreLogger; protected statusTables: LazyMap>>; - protected viewedModulesTables: LazyMap>>; + protected viewedModulesTables: LazyMap< + AsyncInstance> + >; constructor() { this.logger = CoreLogger.getInstance('CoreCourseProvider'); @@ -137,12 +144,16 @@ export class CoreCourseProvider { this.viewedModulesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(COURSE_VIEWED_MODULES_TABLE, { - siteId, - config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, - primaryKeyColumns: ['courseId', 'cmId'], - onDestroy: () => delete this.viewedModulesTables[siteId], - }), + () => CoreSites.getSiteTable( + COURSE_VIEWED_MODULES_TABLE, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + primaryKeyColumns: [...COURSE_VIEWED_MODULES_PRIMARY_KEYS], + rowIdColumn: null, + onDestroy: () => delete this.viewedModulesTables[siteId], + }, + ), ), ); } diff --git a/src/core/features/course/services/database/course.ts b/src/core/features/course/services/database/course.ts index 7762d0b912c..0c114e1e0fe 100644 --- a/src/core/features/course/services/database/course.ts +++ b/src/core/features/course/services/database/course.ts @@ -19,6 +19,7 @@ import { CoreSiteSchema } from '@services/sites'; */ export const COURSE_STATUS_TABLE = 'course_status'; export const COURSE_VIEWED_MODULES_TABLE = 'course_viewed_modules'; +export const COURSE_VIEWED_MODULES_PRIMARY_KEYS = ['courseId', 'cmId'] as const; export const SITE_SCHEMA: CoreSiteSchema = { name: 'CoreCourseProvider', version: 2, @@ -75,7 +76,7 @@ export const SITE_SCHEMA: CoreSiteSchema = { type: 'INTEGER', }, ], - primaryKeys: ['courseId', 'cmId'], + primaryKeys: [...COURSE_VIEWED_MODULES_PRIMARY_KEYS], }, ], }; @@ -133,6 +134,8 @@ export type CoreCourseViewedModulesDBRecord = { sectionId?: number; }; +export type CoreCourseViewedModulesDBPrimaryKeys = typeof COURSE_VIEWED_MODULES_PRIMARY_KEYS[number]; + export type CoreCourseManualCompletionDBRecord = { cmid: number; completed: number; diff --git a/src/core/features/pushnotifications/services/database/pushnotifications.ts b/src/core/features/pushnotifications/services/database/pushnotifications.ts index d1e32a531f1..37d851bba03 100644 --- a/src/core/features/pushnotifications/services/database/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/database/pushnotifications.ts @@ -21,8 +21,10 @@ import { CoreSiteSchema } from '@services/sites'; * Keep "addon" in some names for backwards compatibility. */ export const BADGE_TABLE_NAME = 'addon_pushnotifications_badge'; +export const BADGE_TABLE_PRIMARY_KEYS = ['siteid', 'addon'] as const; export const PENDING_UNREGISTER_TABLE_NAME = 'addon_pushnotifications_pending_unregister'; export const REGISTERED_DEVICES_TABLE_NAME = 'addon_pushnotifications_registered_devices_2'; +export const REGISTERED_DEVICES_TABLE_PRIMARY_KEYS = ['appid', 'uuid'] as const; export const APP_SCHEMA: CoreAppSchema = { name: 'CorePushNotificationsProvider', version: 1, @@ -43,7 +45,7 @@ export const APP_SCHEMA: CoreAppSchema = { type: 'INTEGER', }, ], - primaryKeys: ['siteid', 'addon'], + primaryKeys: [...BADGE_TABLE_PRIMARY_KEYS], }, { name: PENDING_UNREGISTER_TABLE_NAME, @@ -109,7 +111,7 @@ export const SITE_SCHEMA: CoreSiteSchema = { type: 'TEXT', }, ], - primaryKeys: ['appid', 'uuid'], + primaryKeys: [...REGISTERED_DEVICES_TABLE_PRIMARY_KEYS], }, ], async migrate(db: SQLiteDB, oldVersion: number): Promise { @@ -129,6 +131,8 @@ export type CorePushNotificationsBadgeDBRecord = { number: number; // eslint-disable-line id-blacklist }; +export type CorePushNotificationsBadgeDBPrimaryKeys = typeof BADGE_TABLE_PRIMARY_KEYS[number]; + /** * Data stored in DB for pending unregisters. */ @@ -152,3 +156,5 @@ export type CorePushNotificationsRegisteredDeviceDBRecord = { pushid: string; // Push ID. publickey?: string; // Public key. }; + +export type CorePushNotificationsRegisteredDeviceDBPrimaryKeys = typeof REGISTERED_DEVICES_TABLE_PRIMARY_KEYS[number]; diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index bfcb5dbca09..8a5a9d66e26 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -36,6 +36,10 @@ import { CorePushNotificationsPendingUnregisterDBRecord, CorePushNotificationsRegisteredDeviceDBRecord, CorePushNotificationsBadgeDBRecord, + REGISTERED_DEVICES_TABLE_PRIMARY_KEYS, + CorePushNotificationsRegisteredDeviceDBPrimaryKeys, + CorePushNotificationsBadgeDBPrimaryKeys, + BADGE_TABLE_PRIMARY_KEYS, } from './database/pushnotifications'; import { CoreError } from '@classes/errors/error'; import { CoreWSExternalWarning } from '@services/ws'; @@ -61,23 +65,38 @@ export class CorePushNotificationsProvider { protected logger: CoreLogger; protected pushID?: string; - protected badgesTable = asyncInstance>(); + protected badgesTable = + asyncInstance>(); + protected pendingUnregistersTable = asyncInstance>(); protected registeredDevicesTables: - LazyMap>>; + LazyMap< + AsyncInstance< + CoreDatabaseTable< + CorePushNotificationsRegisteredDeviceDBRecord, + CorePushNotificationsRegisteredDeviceDBPrimaryKeys, + never + > + > + >; constructor() { this.logger = CoreLogger.getInstance('CorePushNotificationsProvider'); this.registeredDevicesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable( + () => CoreSites.getSiteTable< + CorePushNotificationsRegisteredDeviceDBRecord, + CorePushNotificationsRegisteredDeviceDBPrimaryKeys, + never + >( REGISTERED_DEVICES_TABLE_NAME, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, - primaryKeyColumns: ['appid', 'uuid'], + primaryKeyColumns: [...REGISTERED_DEVICES_TABLE_PRIMARY_KEYS], + rowIdColumn: null, onDestroy: () => delete this.registeredDevicesTables[siteId], }, ), @@ -190,11 +209,11 @@ export class CorePushNotificationsProvider { } const database = CoreApp.getDB(); - const badgesTable = new CoreDatabaseTableProxy( + const badgesTable = new CoreDatabaseTableProxy( { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, database, BADGE_TABLE_NAME, - ['siteid', 'addon'], + [...BADGE_TABLE_PRIMARY_KEYS], ); const pendingUnregistersTable = new CoreDatabaseTableProxy( { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, diff --git a/src/core/features/usertours/services/user-tours.ts b/src/core/features/usertours/services/user-tours.ts index 2dfd3fde61c..3afbe03c4a5 100644 --- a/src/core/features/usertours/services/user-tours.ts +++ b/src/core/features/usertours/services/user-tours.ts @@ -49,7 +49,6 @@ export class CoreUserToursService { { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, CoreApp.getDB(), USER_TOURS_TABLE_NAME, - ['id'], ); await table.initialize(); diff --git a/src/core/services/database/filepool.ts b/src/core/services/database/filepool.ts index 710584800fc..af922424585 100644 --- a/src/core/services/database/filepool.ts +++ b/src/core/services/database/filepool.ts @@ -19,8 +19,10 @@ import { CoreSiteSchema } from '@services/sites'; * Database variables for CoreFilepool service. */ export const QUEUE_TABLE_NAME = 'filepool_files_queue'; // Queue of files to download. +export const QUEUE_TABLE_PRIMARY_KEYS = ['siteId', 'fileId'] as const; export const FILES_TABLE_NAME = 'filepool_files'; // Downloaded files. export const LINKS_TABLE_NAME = 'filepool_files_links'; // Links between downloaded files and components. +export const LINKS_TABLE_PRIMARY_KEYS = ['fileId', 'component', 'componentId'] as const; export const PACKAGES_TABLE_NAME = 'filepool_packages'; // Downloaded packages (sets of files). export const APP_SCHEMA: CoreAppSchema = { name: 'CoreFilepoolProvider', @@ -74,7 +76,7 @@ export const APP_SCHEMA: CoreAppSchema = { type: 'TEXT', }, ], - primaryKeys: ['siteId', 'fileId'], + primaryKeys: [...QUEUE_TABLE_PRIMARY_KEYS], }, ], }; @@ -146,7 +148,7 @@ export const SITE_SCHEMA: CoreSiteSchema = { type: 'TEXT', }, ], - primaryKeys: ['fileId', 'component', 'componentId'], + primaryKeys: [...LINKS_TABLE_PRIMARY_KEYS], }, { name: PACKAGES_TABLE_NAME, @@ -241,7 +243,7 @@ export type CoreFilepoolFileEntry = CoreFilepoolFileOptions & { /** * DB data for entry from file's queue. */ -export type CoreFilepoolQueueDBEntry = CoreFilepoolFileOptions & { +export type CoreFilepoolQueueDBRecord = CoreFilepoolFileOptions & { /** * The site the file belongs to. */ @@ -278,10 +280,12 @@ export type CoreFilepoolQueueDBEntry = CoreFilepoolFileOptions & { links: string; }; +export type CoreFilepoolQueueDBPrimaryKeys = typeof QUEUE_TABLE_PRIMARY_KEYS[number]; + /** * Entry from the file's queue. */ -export type CoreFilepoolQueueEntry = CoreFilepoolQueueDBEntry & { +export type CoreFilepoolQueueEntry = CoreFilepoolQueueDBRecord & { /** * File links (to link the file to components and componentIds). */ @@ -356,8 +360,10 @@ export type CoreFilepoolComponentLink = { /** * Links table record type. */ -export type CoreFilepoolLinksRecord = { +export type CoreFilepoolLinksDBRecord = { fileId: string; // File Id. component: string; // Component name. componentId: number | string; // Component Id. }; + +export type CoreFilepoolLinksDBPrimaryKeys = typeof LINKS_TABLE_PRIMARY_KEYS[number]; diff --git a/src/core/services/database/sites.ts b/src/core/services/database/sites.ts index 7460b348002..8f5bb55b1f6 100644 --- a/src/core/services/database/sites.ts +++ b/src/core/services/database/sites.ts @@ -28,6 +28,7 @@ export const SCHEMA_VERSIONS_TABLE_NAME = 'schema_versions'; export const WS_CACHE_TABLE = 'wscache_2'; export const CONFIG_TABLE = 'core_site_config'; export const LAST_VIEWED_TABLE = 'core_site_last_viewed'; +export const LAST_VIEWED_PRIMARY_KEYS = ['component', 'id'] as const; // Schema to register in App DB. export const APP_SCHEMA: CoreAppSchema = { @@ -156,7 +157,7 @@ export const SITE_SCHEMA: CoreSiteSchema = { type: 'INTEGER', }, ], - primaryKeys: ['component', 'id'], + primaryKeys: [...LAST_VIEWED_PRIMARY_KEYS], }, ], }; @@ -214,3 +215,5 @@ export type CoreSiteLastViewedDBRecord = { timeaccess: number; data?: string; }; + +export type CoreSiteLastViewedDBPrimaryKeys = typeof LAST_VIEWED_PRIMARY_KEYS[number]; diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index f4f1fc7644c..2e4874e06ee 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -42,10 +42,14 @@ import { CoreFilepoolFileEntry, CoreFilepoolComponentLink, CoreFilepoolFileOptions, - CoreFilepoolLinksRecord, + CoreFilepoolLinksDBRecord, CoreFilepoolPackageEntry, CoreFilepoolQueueEntry, - CoreFilepoolQueueDBEntry, + CoreFilepoolQueueDBRecord, + CoreFilepoolLinksDBPrimaryKeys, + LINKS_TABLE_PRIMARY_KEYS, + CoreFilepoolQueueDBPrimaryKeys, + QUEUE_TABLE_PRIMARY_KEYS, } from '@services/database/filepool'; import { CoreFileHelper } from './file-helper'; import { CoreUrl } from '@singletons/url'; @@ -102,33 +106,39 @@ export class CoreFilepoolProvider { // Variables to prevent downloading packages/files twice at the same time. protected packagesPromises: { [s: string]: { [s: string]: Promise } } = {}; protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; - protected filesTables: LazyMap>>; - protected linksTables: - LazyMap>>; + protected filesTables: LazyMap>>; + protected linksTables: LazyMap< + AsyncInstance> + >; protected packagesTables: LazyMap>>; - protected queueTable = asyncInstance>(); + protected queueTable = asyncInstance>(); constructor() { this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); this.filesTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(FILES_TABLE_NAME, { + () => CoreSites.getSiteTable(FILES_TABLE_NAME, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, primaryKeyColumns: ['fileId'], + rowIdColumn: null, onDestroy: () => delete this.filesTables[siteId], }), ), ); this.linksTables = lazyMap( siteId => asyncInstance( - () => CoreSites.getSiteTable(LINKS_TABLE_NAME, { - siteId, - config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, - primaryKeyColumns: ['fileId', 'component', 'componentId'], - onDestroy: () => delete this.linksTables[siteId], - }), + () => CoreSites.getSiteTable( + LINKS_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, + primaryKeyColumns: [...LINKS_TABLE_PRIMARY_KEYS], + rowIdColumn: null, + onDestroy: () => delete this.linksTables[siteId], + }, + ), ), ); this.packagesTables = lazyMap( @@ -168,11 +178,11 @@ export class CoreFilepoolProvider { // Ignore errors. } - const queueTable = new CoreDatabaseTableProxy( + const queueTable = new CoreDatabaseTableProxy( { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, CoreApp.getDB(), QUEUE_TABLE_NAME, - ['siteId','fileId'], + [...QUEUE_TABLE_PRIMARY_KEYS], ); await queueTable.initialize(); @@ -406,7 +416,7 @@ export class CoreFilepoolProvider { return this.addToQueue(siteId, fileId, fileUrl, priority, revision, timemodified, filePath, onProgress, options, link); } - const newData: Partial = {}; + const newData: Partial = {}; let foundLink = false; // We already have the file in queue, we update the priority and links. @@ -1245,7 +1255,7 @@ export class CoreFilepoolProvider { siteId: string | undefined, component: string, componentId?: string | number, - ): Promise { + ): Promise { siteId = siteId ?? CoreSites.getCurrentSiteId(); const conditions = { component, @@ -1364,7 +1374,7 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @returns Promise resolved with the links. */ - protected async getFileLinks(siteId: string, fileId: string): Promise { + protected async getFileLinks(siteId: string, fileId: string): Promise { const items = await this.linksTables[siteId].getMany({ fileId }); items.forEach((item) => { diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index d5dd45603af..4119500de35 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -93,7 +93,7 @@ export class CoreSitesProvider { protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; protected pluginsSiteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; protected siteTables: Record>> = {}; - protected schemasTables: Record>> = {}; + protected schemasTables: Record>> = {}; protected sitesTable = asyncInstance>(); constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] | null) { @@ -211,7 +211,8 @@ export class CoreSitesProvider { */ async getSiteTable< DBRecord extends SQLiteDBRecordValues, - PrimaryKeyColumn extends keyof DBRecord + PrimaryKeyColumn extends keyof DBRecord, + RowIdColumn extends PrimaryKeyColumn, >( tableName: string, options: Partial<{ @@ -219,9 +220,10 @@ export class CoreSitesProvider { config: Partial; database: SQLiteDB; primaryKeyColumns: PrimaryKeyColumn[]; + rowIdColumn: RowIdColumn | null; onDestroy(): void; }> = {}, - ): Promise> { + ): Promise> { const siteId = options.siteId ?? this.getCurrentSiteId(); if (!(siteId in this.siteTables)) { @@ -231,11 +233,12 @@ export class CoreSitesProvider { if (!(tableName in this.siteTables[siteId])) { const promisedTable = this.siteTables[siteId][tableName] = new CorePromisedValue(); const database = options.database ?? await this.getSiteDb(siteId); - const table = new CoreDatabaseTableProxy( + const table = new CoreDatabaseTableProxy( options.config ?? {}, database, tableName, options.primaryKeyColumns, + options.rowIdColumn, ); options.onDestroy && table.addListener({ onDestroy: options.onDestroy }); @@ -245,7 +248,7 @@ export class CoreSitesProvider { promisedTable.resolve(table as unknown as CoreDatabaseTable); } - return this.siteTables[siteId][tableName] as unknown as Promise>; + return this.siteTables[siteId][tableName] as unknown as Promise>; } /** @@ -2033,6 +2036,7 @@ export class CoreSitesProvider { database: site.getDb(), config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, primaryKeyColumns: ['name'], + rowIdColumn: null, onDestroy: () => delete this.schemasTables[siteId], }), ); From 4d4a506fe1dd6c9ac43ccbd7913b04e297918e74 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 12:19:45 +0100 Subject: [PATCH 04/19] MOBILE-4304 reminders: Update database usage --- .../classes/database/database-table-proxy.ts | 7 ++ .../features/reminders/services/reminders.ts | 69 +++++++++++++------ 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index 4bf1ede74ca..64762a0cf2c 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -159,6 +159,13 @@ export class CoreDatabaseTableProxy< return this.target.insert(record); } + /** + * @inheritdoc + */ + syncInsert(record: Omit & Partial>): void { + this.target.syncInsert(record); + } + /** * @inheritdoc */ diff --git a/src/core/features/reminders/services/reminders.ts b/src/core/features/reminders/services/reminders.ts index 75c846c5469..d9e995f3bd0 100644 --- a/src/core/features/reminders/services/reminders.ts +++ b/src/core/features/reminders/services/reminders.ts @@ -23,6 +23,10 @@ import { CorePlatform } from '@services/platform'; import { CoreConstants } from '@/core/constants'; import { CoreConfig } from '@services/config'; import { CoreEvents } from '@singletons/events'; +import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Units to set a reminder. @@ -61,6 +65,20 @@ export class CoreRemindersService { static readonly DEFAULT_NOTIFICATION_TIME_SETTING = 'CoreRemindersDefaultNotification'; static readonly DEFAULT_NOTIFICATION_TIME_CHANGED = 'CoreRemindersDefaultNotificationChangedEvent'; + protected remindersTables: LazyMap>>; + + constructor() { + this.remindersTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(REMINDERS_TABLE, { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.remindersTables[siteId], + }), + ), + ); + } + /** * Initialize the service. * @@ -103,13 +121,13 @@ export class CoreRemindersService { * @returns Resolved when done. Rejected on failure. */ async addReminder(reminder: CoreReminderData, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - const reminderId = await site.getDb().insertRecord(REMINDERS_TABLE, reminder); + const reminderId = await this.remindersTables[siteId].insert(reminder); const reminderRecord: CoreReminderDBRecord = Object.assign(reminder, { id: reminderId }); - await this.scheduleNotification(reminderRecord, site.getId()); + await this.scheduleNotification(reminderRecord, siteId); } /** @@ -123,9 +141,9 @@ export class CoreRemindersService { reminder: CoreReminderDBRecord, siteId?: string, ): Promise { - const site = await CoreSites.getSite(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - await site.getDb().updateRecords(REMINDERS_TABLE, reminder, { id: reminder.id }); + await this.remindersTables[siteId].update(reminder, { id: reminder.id }); // Reschedule. await this.scheduleNotification(reminder, siteId); @@ -162,9 +180,13 @@ export class CoreRemindersService { * @returns Promise resolved when the reminder data is retrieved. */ async getAllReminders(siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - return site.getDb().getRecords(REMINDERS_TABLE, undefined, 'time ASC'); + return this.remindersTables[siteId].getMany(undefined, { + sorting: [ + { time: 'asc' }, + ], + }); } /** @@ -175,9 +197,13 @@ export class CoreRemindersService { * @returns Promise resolved when the reminder data is retrieved. */ async getReminders(selector: CoreReminderSelector, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - return site.getDb().getRecords(REMINDERS_TABLE, selector, 'time ASC'); + return this.remindersTables[siteId].getMany(selector, { + sorting: [ + { time: 'asc' }, + ], + }); } /** @@ -187,13 +213,13 @@ export class CoreRemindersService { * @returns Promise resolved when the reminder data is retrieved. */ protected async getRemindersWithDefaultTime(siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - return site.getDb().getRecords( - REMINDERS_TABLE, - { timebefore: CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE }, - 'time ASC', - ); + return this.remindersTables[siteId].getMany({ timebefore: CoreRemindersService.DEFAULT_REMINDER_TIMEBEFORE }, { + sorting: [ + { time: 'asc' }, + ], + }); } /** @@ -204,15 +230,15 @@ export class CoreRemindersService { * @returns Promise resolved when the notification is updated. */ async removeReminder(id: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - const reminder = await site.getDb().getRecord(REMINDERS_TABLE, { id }); + const reminder = await this.remindersTables[siteId].getOneByPrimaryKey({ id }); if (this.isEnabled()) { - this.cancelReminder(id, reminder.component, site.getId()); + this.cancelReminder(id, reminder.component, siteId); } - await site.getDb().deleteRecords(REMINDERS_TABLE, { id }); + await this.remindersTables[siteId].deleteByPrimaryKey({ id }); } /** @@ -223,8 +249,7 @@ export class CoreRemindersService { * @returns Promise resolved when the notification is updated. */ async removeReminders(selector: CoreReminderSelector, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - siteId = site.getId(); + siteId ??= CoreSites.getCurrentSiteId(); if (this.isEnabled()) { const reminders = await this.getReminders(selector, siteId); @@ -234,7 +259,7 @@ export class CoreRemindersService { }); } - await site.getDb().deleteRecords(REMINDERS_TABLE, selector); + await this.remindersTables[siteId].delete(selector); } /** From 7e2d2fb74b5c40142ba733dd41538d555e0fb834 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 12:46:50 +0100 Subject: [PATCH 05/19] MOBILE-4304 search: Update database usage --- .../search/services/search-history-db.ts | 5 +- .../search/services/search-history.service.ts | 71 ++++++++++++------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/core/features/search/services/search-history-db.ts b/src/core/features/search/services/search-history-db.ts index 19a1e88193c..42f034fa3f2 100644 --- a/src/core/features/search/services/search-history-db.ts +++ b/src/core/features/search/services/search-history-db.ts @@ -18,6 +18,7 @@ import { CoreSiteSchema } from '@services/sites'; * Database variables for CoreSearchHistory service. */ export const SEARCH_HISTORY_TABLE_NAME = 'seach_history'; +export const SEARCH_HISTORY_TABLE_PRIMARY_KEYS = ['searcharea', 'searchedtext'] as const; export const SITE_SCHEMA: CoreSiteSchema = { name: 'CoreSearchHistoryProvider', version: 1, @@ -46,7 +47,7 @@ export const SITE_SCHEMA: CoreSiteSchema = { notNull: true, }, ], - primaryKeys: ['searcharea', 'searchedtext'], + primaryKeys: [...SEARCH_HISTORY_TABLE_PRIMARY_KEYS], }, ], }; @@ -60,3 +61,5 @@ export type CoreSearchHistoryDBRecord = { searchedtext: string; // Text of the performed search. times: number; // Times search has been performed (if previously in history). }; + +export type CoreSearchHistoryDBPrimaryKeys = typeof SEARCH_HISTORY_TABLE_PRIMARY_KEYS[number]; diff --git a/src/core/features/search/services/search-history.service.ts b/src/core/features/search/services/search-history.service.ts index f27c9786edf..6c4a48445b1 100644 --- a/src/core/features/search/services/search-history.service.ts +++ b/src/core/features/search/services/search-history.service.ts @@ -15,9 +15,17 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { SQLiteDB } from '@classes/sqlitedb'; -import { CoreSearchHistoryDBRecord, SEARCH_HISTORY_TABLE_NAME } from './search-history-db'; +import { + CoreSearchHistoryDBPrimaryKeys, + CoreSearchHistoryDBRecord, + SEARCH_HISTORY_TABLE_NAME, + SEARCH_HISTORY_TABLE_PRIMARY_KEYS, +} from './search-history-db'; import { makeSingleton } from '@singletons'; +import { LazyMap, lazyMap } from '@/core/utils/lazy-map'; +import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Service that enables adding a history to a search box. @@ -27,6 +35,27 @@ export class CoreSearchHistoryProvider { protected static readonly HISTORY_LIMIT = 10; + protected searchHistoryTables: LazyMap< + AsyncInstance> + >; + + constructor() { + this.searchHistoryTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + SEARCH_HISTORY_TABLE_NAME, + { + siteId, + primaryKeyColumns: [...SEARCH_HISTORY_TABLE_PRIMARY_KEYS], + rowIdColumn: null, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.searchHistoryTables[siteId], + }, + ), + ), + ); + } + /** * Get a search area history sorted by use. * @@ -35,12 +64,9 @@ export class CoreSearchHistoryProvider { * @returns Promise resolved with the list of items when done. */ async getSearchHistory(searchArea: string, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - const conditions = { - searcharea: searchArea, - }; + siteId ??= CoreSites.getCurrentSiteId(); - const history: CoreSearchHistoryDBRecord[] = await site.getDb().getRecords(SEARCH_HISTORY_TABLE_NAME, conditions); + const history = await this.searchHistoryTables[siteId].getMany({ searcharea: searchArea }); // Sorting by last used DESC. return history.sort((a, b) => (b.lastused || 0) - (a.lastused || 0)); @@ -50,10 +76,10 @@ export class CoreSearchHistoryProvider { * Controls search limit and removes the last item if overflows. * * @param searchArea Search area to control - * @param db SQLite DB where to perform the search. + * @param siteId Site id. * @returns Resolved when done. */ - protected async controlSearchLimit(searchArea: string, db: SQLiteDB): Promise { + protected async controlSearchLimit(searchArea: string, siteId: string): Promise { const items = await this.getSearchHistory(searchArea); if (items.length > CoreSearchHistoryProvider.HISTORY_LIMIT) { // Over the limit. Remove the last. @@ -62,12 +88,10 @@ export class CoreSearchHistoryProvider { return; } - const searchItem = { + await this.searchHistoryTables[siteId].delete({ searcharea: lastItem.searcharea, searchedtext: lastItem.searchedtext, - }; - - await db.deleteRecords(SEARCH_HISTORY_TABLE_NAME, searchItem); + }); } } @@ -76,23 +100,23 @@ export class CoreSearchHistoryProvider { * * @param searchArea Area where the search has been performed. * @param text Text of the performed text. - * @param db SQLite DB where to perform the search. + * @param siteId Site id. * @returns True if exists, false otherwise. */ - protected async updateExistingItem(searchArea: string, text: string, db: SQLiteDB): Promise { + protected async updateExistingItem(searchArea: string, text: string, siteId: string): Promise { const searchItem = { searcharea: searchArea, searchedtext: text, }; try { - const existingItem: CoreSearchHistoryDBRecord = await db.getRecord(SEARCH_HISTORY_TABLE_NAME, searchItem); + const existingItem = await this.searchHistoryTables[siteId].getOne(searchItem); // If item exist, update time and number of times searched. existingItem.lastused = Date.now(); existingItem.times++; - await db.updateRecords(SEARCH_HISTORY_TABLE_NAME, existingItem, searchItem); + await this.searchHistoryTables[siteId].update(existingItem, searchItem); return true; } catch { @@ -109,23 +133,20 @@ export class CoreSearchHistoryProvider { * @returns Resolved when done. */ async insertOrUpdateSearchText(searchArea: string, text: string, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - const db = site.getDb(); + siteId ??= CoreSites.getCurrentSiteId(); - const exists = await this.updateExistingItem(searchArea, text, db); + const exists = await this.updateExistingItem(searchArea, text, siteId); if (!exists) { // If item is new, control the history does not goes over the limit. - const searchItem: CoreSearchHistoryDBRecord = { + await this.searchHistoryTables[siteId].insert({ searcharea: searchArea, searchedtext: text, lastused: Date.now(), times: 1, - }; - - await db.insertRecord(SEARCH_HISTORY_TABLE_NAME, searchItem); + }); - await this.controlSearchLimit(searchArea, db); + await this.controlSearchLimit(searchArea, siteId); } } From bb82cd8ff1ca6881ccda5b96580b2e2cdac9cbeb Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 15:20:01 +0100 Subject: [PATCH 06/19] MOBILE-4304 sharedfiles: Update database usage --- .../sharedfiles/services/sharedfiles.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/core/features/sharedfiles/services/sharedfiles.ts b/src/core/features/sharedfiles/services/sharedfiles.ts index 5d8e5231a51..2cc359bb3e5 100644 --- a/src/core/features/sharedfiles/services/sharedfiles.ts +++ b/src/core/features/sharedfiles/services/sharedfiles.ts @@ -16,7 +16,6 @@ import { Injectable } from '@angular/core'; import { FileEntry, DirectoryEntry } from '@awesome-cordova-plugins/file/ngx'; import { Md5 } from 'ts-md5/dist/md5'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreLogger } from '@singletons/logger'; import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; @@ -27,6 +26,9 @@ import { CoreEvents } from '@singletons/events'; import { makeSingleton } from '@singletons'; import { APP_SCHEMA, CoreSharedFilesDBRecord, SHARED_FILES_TABLE_NAME } from './database/sharedfiles'; import { CorePath } from '@singletons/path'; +import { asyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; /** * Service to share files with the app. @@ -37,13 +39,10 @@ export class CoreSharedFilesProvider { static readonly SHARED_FILES_FOLDER = 'sharedfiles'; protected logger: CoreLogger; - // Variables for DB. - protected appDB: Promise; - protected resolveAppDB!: (appDB: SQLiteDB) => void; + protected sharedFilesTable = asyncInstance>(); constructor() { this.logger = CoreLogger.getInstance('CoreSharedFilesProvider'); - this.appDB = new Promise(resolve => this.resolveAppDB = resolve); } /** @@ -58,7 +57,16 @@ export class CoreSharedFilesProvider { // Ignore errors. } - this.resolveAppDB(CoreApp.getDB()); + const database = CoreApp.getDB(); + const sharedFilesTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.None }, + database, + SHARED_FILES_TABLE_NAME, + ); + + await sharedFilesTable.initialize(); + + this.sharedFilesTable.setInstance(sharedFilesTable); } /** @@ -199,9 +207,9 @@ export class CoreSharedFilesProvider { * @returns Resolved if treated, rejected otherwise. */ protected async isFileTreated(fileId: string): Promise { - const db = await this.appDB; + const sharedFile = await this.sharedFilesTable.getOneByPrimaryKey({ id: fileId }); - return db.getRecord(SHARED_FILES_TABLE_NAME, { id: fileId }); + return sharedFile; } /** @@ -216,9 +224,7 @@ export class CoreSharedFilesProvider { await this.isFileTreated(fileId); } catch (err) { // Doesn't exist, insert it. - const db = await this.appDB; - - await db.insertRecord(SHARED_FILES_TABLE_NAME, { id: fileId }); + await this.sharedFilesTable.insert({ id: fileId }); } } @@ -259,9 +265,7 @@ export class CoreSharedFilesProvider { * @returns Resolved when unmarked. */ protected async unmarkAsTreated(fileId: string): Promise { - const db = await this.appDB; - - await db.deleteRecords(SHARED_FILES_TABLE_NAME, { id: fileId }); + await this.sharedFilesTable.deleteByPrimaryKey({ id: fileId }); } } From 807860f0d5403f12730098fe52464d46c97b4bec Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 15:45:33 +0100 Subject: [PATCH 07/19] MOBILE-4304 core: Update database usage --- .../services/database/local-notifications.ts | 17 ++- src/core/services/local-notifications.ts | 105 ++++++++++++------ 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/core/services/database/local-notifications.ts b/src/core/services/database/local-notifications.ts index b8c735a938b..80326c6f383 100644 --- a/src/core/services/database/local-notifications.ts +++ b/src/core/services/database/local-notifications.ts @@ -74,7 +74,22 @@ export const APP_SCHEMA: CoreAppSchema = { }; export type CodeRequestsQueueItem = { - table: string; + table: typeof SITES_TABLE_NAME | typeof COMPONENTS_TABLE_NAME; id: string; deferreds: CorePromisedValue[]; }; + +export type CoreLocalNotificationsSitesDBRecord = { + id: string; + code: number; +}; + +export type CoreLocalNotificationsComponentsDBRecord = { + id: string; + code: number; +}; + +export type CoreLocalNotificationsTriggeredDBRecord = { + id: number; + at: number; +}; diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts index f7cc1b4baa1..f6ea2cdd58e 100644 --- a/src/core/services/local-notifications.ts +++ b/src/core/services/local-notifications.ts @@ -20,7 +20,6 @@ import { CoreApp } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreTextUtils } from '@services/utils/text'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; @@ -32,10 +31,16 @@ import { COMPONENTS_TABLE_NAME, SITES_TABLE_NAME, CodeRequestsQueueItem, + CoreLocalNotificationsTriggeredDBRecord, + CoreLocalNotificationsComponentsDBRecord, + CoreLocalNotificationsSitesDBRecord, } from '@services/database/local-notifications'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { Push } from '@features/native/plugins'; +import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; /** * Service to handle local notifications. @@ -56,12 +61,11 @@ export class CoreLocalNotificationsProvider { protected updateSubscription?: Subscription; protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477). - // Variables for DB. - protected appDB: Promise; - protected resolveAppDB!: (appDB: SQLiteDB) => void; + protected sitesTable = asyncInstance>(); + protected componentsTable = asyncInstance>(); + protected triggeredTable = asyncInstance>(); constructor() { - this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider'); this.queueRunner = new CoreQueueRunner(10); } @@ -127,7 +131,36 @@ export class CoreLocalNotificationsProvider { // Ignore errors. } - this.resolveAppDB(CoreApp.getDB()); + const database = CoreApp.getDB(); + const sitesTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.None }, + database, + SITES_TABLE_NAME, + ['id'], + null, + ); + const componentsTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.None }, + database, + COMPONENTS_TABLE_NAME, + ['id'], + null, + ); + const triggeredTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.None }, + database, + TRIGGERED_TABLE_NAME, + ); + + await Promise.all([ + sitesTable.initialize(), + componentsTable.initialize(), + triggeredTable.initialize(), + ]); + + this.sitesTable.setInstance(sitesTable); + this.componentsTable.setInstance(componentsTable); + this.triggeredTable.setInstance(triggeredTable); } /** @@ -222,7 +255,10 @@ export class CoreLocalNotificationsProvider { * @param id ID of the element to get its code. * @returns Promise resolved when the code is retrieved. */ - protected async getCode(table: string, id: string): Promise { + protected async getCode( + table: AsyncInstance>, + id: string, + ): Promise { const key = table + '#' + id; // Check if the code is already in memory. @@ -230,25 +266,27 @@ export class CoreLocalNotificationsProvider { return this.codes[key]; } - const db = await this.appDB; - try { // Check if we already have a code stored for that ID. - const entry = await db.getRecord<{id: string; code: number}>(table, { id: id }); + const entry = await table.getOneByPrimaryKey({ id: id }); this.codes[key] = entry.code; return entry.code; } catch (err) { // No code stored for that ID. Create a new code for it. - const entries = await db.getRecords<{id: string; code: number}>(table, undefined, 'code DESC'); + const entries = await table.getMany(undefined, { + sorting: [ + { code: 'desc' }, + ], + }); let newCode = 0; if (entries.length > 0) { newCode = entries[0].code + 1; } - await db.insertRecord(table, { id: id, code: newCode }); + await table.insert({ id: id, code: newCode }); this.codes[key] = newCode; return newCode; @@ -347,13 +385,12 @@ export class CoreLocalNotificationsProvider { * @returns Promise resolved with a boolean indicating if promise is triggered (true) or not. */ async isTriggered(notification: ILocalNotification, useQueue: boolean = true): Promise { - const db = await this.appDB; + if (notification.id === undefined) { + return false; + } try { - const stored = await db.getRecord<{ id: number; at: number }>( - TRIGGERED_TABLE_NAME, - { id: notification.id }, - ); + const stored = await this.triggeredTable.getOneByPrimaryKey({ id: notification.id }); let triggered = (notification.trigger && notification.trigger.at) || 0; @@ -439,7 +476,18 @@ export class CoreLocalNotificationsProvider { } // Get the code and resolve/reject all the promises of this request. - const code = await this.getCode(request.table, request.id); + const getCodeFromTable = async () => { + switch (request.table) { + case SITES_TABLE_NAME: + return this.getCode(this.sitesTable, request.id); + case COMPONENTS_TABLE_NAME: + return this.getCode(this.componentsTable, request.id); + default: + throw new Error(`Unknown local-notifications table: ${request.table}`); + } + }; + + const code = await getCodeFromTable(); request.deferreds.forEach((p) => { p.resolve(code); @@ -506,9 +554,7 @@ export class CoreLocalNotificationsProvider { * @returns Promise resolved when it is removed. */ async removeTriggered(id: number): Promise { - const db = await this.appDB; - - await db.deleteRecords(TRIGGERED_TABLE_NAME, { id: id }); + await this.triggeredTable.deleteByPrimaryKey({ id }); } /** @@ -518,7 +564,7 @@ export class CoreLocalNotificationsProvider { * @param id ID of the element to get its code. * @returns Promise resolved when the code is retrieved. */ - protected requestCode(table: string, id: string): Promise { + protected requestCode(table: typeof SITES_TABLE_NAME | typeof COMPONENTS_TABLE_NAME, id: string): Promise { const deferred = new CorePromisedValue(); const key = table + '#' + id; const isQueueEmpty = Object.keys(this.codeRequestsQueue).length == 0; @@ -529,8 +575,8 @@ export class CoreLocalNotificationsProvider { } else { // Add a pending request to the queue. this.codeRequestsQueue[key] = { - table: table, - id: id, + table, + id, deferreds: [deferred], }; } @@ -664,7 +710,6 @@ export class CoreLocalNotificationsProvider { * @returns Promise resolved when stored, rejected otherwise. */ async trigger(notification: ILocalNotification): Promise { - const db = await this.appDB; let time = Date.now(); if (notification.trigger?.at) { // The type says "at" is a Date, but in Android we can receive timestamps instead. @@ -675,12 +720,10 @@ export class CoreLocalNotificationsProvider { } } - const entry = { + return this.triggeredTable.insert({ id: notification.id, at: time, - }; - - return db.insertRecord(TRIGGERED_TABLE_NAME, entry); + }); } /** @@ -691,12 +734,10 @@ export class CoreLocalNotificationsProvider { * @returns Promise resolved when done. */ async updateComponentName(oldName: string, newName: string): Promise { - const db = await this.appDB; - const oldId = COMPONENTS_TABLE_NAME + '#' + oldName; const newId = COMPONENTS_TABLE_NAME + '#' + newName; - await db.updateRecords(COMPONENTS_TABLE_NAME, { id: newId }, { id: oldId }); + await this.componentsTable.update({ id: newId }, { id: oldId }); } } From ef88336a2da2ac40c531776734ca7f801aad03a2 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 17:00:26 +0100 Subject: [PATCH 08/19] MOBILE-4304 h5p: Update database usage --- .../classes/database/database-table-proxy.ts | 7 + src/core/classes/database/database-table.ts | 12 + .../classes/database/debug-database-table.ts | 9 + .../classes/database/eager-database-table.ts | 19 +- .../classes/database/lazy-database-table.ts | 15 ++ src/core/features/h5p/classes/framework.ts | 245 +++++++++++------- 6 files changed, 215 insertions(+), 92 deletions(-) diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index 64762a0cf2c..7fd63308c7e 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -187,6 +187,13 @@ export class CoreDatabaseTableProxy< return this.target.delete(conditions); } + /** + * @inheritdoc + */ + async deleteWhere(conditions: CoreDatabaseConditions): Promise { + return this.target.deleteWhere(conditions); + } + /** * @inheritdoc */ diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index 5cb2131fbe6..c52cb7352e6 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -311,6 +311,18 @@ export class CoreDatabaseTable< : await this.database.deleteRecords(this.tableName); } + /** + * Delete records matching the given conditions. + * + * This method should be used when it's necessary to apply complex conditions; the simple `delete` + * method should be favored otherwise for better performance. + * + * @param conditions Matching conditions in SQL and JavaScript. + */ + async deleteWhere(conditions: CoreDatabaseConditions): Promise { + await this.database.deleteRecordsSelect(this.tableName, conditions.sql, conditions.sqlParams); + } + /** * Delete a single record identified by its primary key. * diff --git a/src/core/classes/database/debug-database-table.ts b/src/core/classes/database/debug-database-table.ts index c3906df25e1..cdef3494700 100644 --- a/src/core/classes/database/debug-database-table.ts +++ b/src/core/classes/database/debug-database-table.ts @@ -186,6 +186,15 @@ export class CoreDebugDatabaseTable< return this.target.delete(conditions); } + /** + * @inheritdoc + */ + async deleteWhere(conditions: CoreDatabaseConditions): Promise { + this.logger.log('deleteWhere', conditions); + + return this.target.deleteWhere(conditions); + } + /** * @inheritdoc */ diff --git a/src/core/classes/database/eager-database-table.ts b/src/core/classes/database/eager-database-table.ts index 3ed457c509f..c5c75f9c986 100644 --- a/src/core/classes/database/eager-database-table.ts +++ b/src/core/classes/database/eager-database-table.ts @@ -202,12 +202,27 @@ export class CoreEagerDatabaseTable< return; } - Object.entries(this.records).forEach(([id, record]) => { + Object.entries(this.records).forEach(([primaryKey, record]) => { if (!this.recordMatches(record, conditions)) { return; } - delete this.records[id]; + delete this.records[primaryKey]; + }); + } + + /** + * @inheritdoc + */ + async deleteWhere(conditions: CoreDatabaseConditions): Promise { + await super.deleteWhere(conditions); + + Object.entries(this.records).forEach(([primaryKey, record]) => { + if (!conditions.js(record)) { + return; + } + + delete record[primaryKey]; }); } diff --git a/src/core/classes/database/lazy-database-table.ts b/src/core/classes/database/lazy-database-table.ts index c8dd76c3179..c0e049e43b3 100644 --- a/src/core/classes/database/lazy-database-table.ts +++ b/src/core/classes/database/lazy-database-table.ts @@ -189,6 +189,21 @@ export class CoreLazyDatabaseTable< } } + /** + * @inheritdoc + */ + async deleteWhere(conditions: CoreDatabaseConditions): Promise { + await super.deleteWhere(conditions); + + Object.entries(this.records).forEach(([primaryKey, record]) => { + if (!record || !conditions.js(record)) { + return; + } + + this.records[primaryKey] = null; + }); + } + /** * @inheritdoc */ diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index 89921a8abc5..fb6c79080ea 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -44,12 +44,85 @@ import { CoreH5PLibraryAddTo, CoreH5PLibraryMetadataSettings } from './validator import { CoreH5PMetadata } from './metadata'; import { Translate } from '@singletons'; import { SQLiteDB } from '@classes/sqlitedb'; +import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; +import { LazyMap, lazyMap } from '@/core/utils/lazy-map'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Equivalent to Moodle's implementation of H5PFrameworkInterface. */ export class CoreH5PFramework { + protected contentTables: LazyMap>>; + protected librariesTables: LazyMap>>; + protected libraryDependenciesTables: LazyMap>>; + protected contentsLibrariesTables: LazyMap>>; + protected librariesCachedAssetsTables: LazyMap>>; + + constructor() { + this.contentTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + CONTENT_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.contentTables[siteId], + }, + ), + ), + ); + this.librariesTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + LIBRARIES_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.librariesTables[siteId], + }, + ), + ), + ); + this.libraryDependenciesTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + LIBRARY_DEPENDENCIES_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.libraryDependenciesTables[siteId], + }, + ), + ), + ); + this.contentsLibrariesTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + CONTENTS_LIBRARIES_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.contentsLibrariesTables[siteId], + }, + ), + ), + ); + this.librariesCachedAssetsTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + LIBRARIES_CACHEDASSETS_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + onDestroy: () => delete this.librariesCachedAssetsTables[siteId], + }, + ), + ), + ); + } + /** * Will clear filtered params for all the content that uses the specified libraries. * This means that the content dependencies will have to be rebuilt and the parameters re-filtered. @@ -63,12 +136,19 @@ export class CoreH5PFramework { return; } - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); const whereAndParams = SQLiteDB.getInOrEqual(libraryIds); whereAndParams.sql = 'mainlibraryid ' + whereAndParams.sql; - await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams.sql, whereAndParams.params); + await this.contentTables[siteId].updateWhere( + { filtered: null }, + { + sql: whereAndParams.sql, + sqlParams: whereAndParams.params, + js: record => libraryIds.includes(record.mainlibraryid), + }, + ); } /** @@ -79,20 +159,19 @@ export class CoreH5PFramework { * @returns Promise resolved with the removed entries. */ async deleteCachedAssets(libraryId: number, siteId?: string): Promise { - - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); // Get all the hashes that use this library. - const entries = await db.getRecords( - LIBRARIES_CACHEDASSETS_TABLE_NAME, - { libraryid: libraryId }, - ); - + const entries = await this.librariesCachedAssetsTables[siteId].getMany({ libraryid: libraryId }); const hashes = entries.map((entry) => entry.hash); if (hashes.length) { // Delete the entries from DB. - await db.deleteRecordsList(LIBRARIES_CACHEDASSETS_TABLE_NAME, 'hash', hashes); + await this.librariesCachedAssetsTables[siteId].deleteWhere({ + sql: hashes.length === 1 ? 'hash = ?' : `hash IN (${hashes.map(() => '?').join(', ')})`, + sqlParams: hashes, + js: (record) => hashes.includes(record.hash), + }); } return entries; @@ -106,8 +185,7 @@ export class CoreH5PFramework { * @returns Promise resolved when done. */ async deleteContentData(id: number, siteId?: string): Promise { - - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); // The user content should be reset (instead of removed), because this method is called when H5P content needs // to be updated too (and the previous states must be kept, but reset). @@ -115,7 +193,7 @@ export class CoreH5PFramework { await Promise.all([ // Delete the content data. - db.deleteRecords(CONTENT_TABLE_NAME, { id }), + this.contentTables[siteId].deleteByPrimaryKey({ id }), // Remove content library dependencies. this.deleteLibraryUsage(id, siteId), @@ -130,9 +208,9 @@ export class CoreH5PFramework { * @returns Promise resolved when done. */ async deleteLibrary(id: number, siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - await db.deleteRecords(LIBRARIES_TABLE_NAME, { id }); + await this.librariesTables[siteId].deleteByPrimaryKey({ id }); } /** @@ -143,9 +221,9 @@ export class CoreH5PFramework { * @returns Promise resolved when done. */ async deleteLibraryDependencies(libraryId: number, siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - await db.deleteRecords(LIBRARY_DEPENDENCIES_TABLE_NAME, { libraryid: libraryId }); + await this.libraryDependenciesTables[siteId].delete({ libraryid: libraryId }); } /** @@ -156,9 +234,9 @@ export class CoreH5PFramework { * @returns Promise resolved when done. */ async deleteLibraryUsage(id: number, siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - await db.deleteRecords(CONTENTS_LIBRARIES_TABLE_NAME, { h5pid: id }); + await this.contentsLibrariesTables[siteId].delete({ h5pid: id }); } /** @@ -168,9 +246,9 @@ export class CoreH5PFramework { * @returns Promise resolved with the list of content data. */ async getAllContentData(siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - return db.getAllRecords(CONTENT_TABLE_NAME); + return this.contentTables[siteId].getMany(); } /** @@ -181,9 +259,9 @@ export class CoreH5PFramework { * @returns Promise resolved with the content data. */ async getContentData(id: number, siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - return db.getRecord(CONTENT_TABLE_NAME, { id }); + return this.contentTables[siteId].getOneByPrimaryKey({ id }); } /** @@ -194,18 +272,16 @@ export class CoreH5PFramework { * @returns Promise resolved with the content data. */ async getContentDataByUrl(fileUrl: string, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - const db = site.getDb(); + siteId ??= CoreSites.getCurrentSiteId(); // Try to use the folder name, it should be more reliable than the URL. - const folderName = await CoreH5P.h5pCore.h5pFS.getContentFolderNameByUrl(fileUrl, site.getId()); + const folderName = await CoreH5P.h5pCore.h5pFS.getContentFolderNameByUrl(fileUrl, siteId); try { - return await db.getRecord(CONTENT_TABLE_NAME, { foldername: folderName }); + return await this.contentTables[siteId].getOne({ foldername: folderName }); } catch (error) { // Cannot get folder name, the h5p file was probably deleted. Just use the URL. - return db.getRecord(CONTENT_TABLE_NAME, { fileurl: fileUrl }); + return await this.contentTables[siteId].getOne({ fileurl: fileUrl }); } } @@ -216,17 +292,19 @@ export class CoreH5PFramework { * @returns Promise resolved with the latest library version data. */ async getLatestLibraryVersion(machineName: string, siteId?: string): Promise { - - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); try { - const records = await db.getRecords( - LIBRARIES_TABLE_NAME, + const records = await this.librariesTables[siteId].getMany( { machinename: machineName }, - 'majorversion DESC, minorversion DESC, patchversion DESC', - '*', - 0, - 1, + { + limit: 1, + sorting: [ + { majorversion: 'desc' }, + { minorversion: 'desc' }, + { patchversion: 'desc' }, + ], + }, ); if (records && records[0]) { @@ -254,13 +332,12 @@ export class CoreH5PFramework { minorVersion?: string | number, siteId?: string, ): Promise { + siteId ??= CoreSites.getCurrentSiteId(); - const db = await CoreSites.getSiteDb(siteId); - - const libraries = await db.getRecords(LIBRARIES_TABLE_NAME, { + const libraries = await this.librariesTables[siteId].getMany({ machinename: machineName, - majorversion: majorVersion, - minorversion: minorVersion, + majorversion: majorVersion !== undefined ? Number(majorVersion) : undefined, + minorversion: minorVersion !== undefined ? Number(minorVersion) : undefined, }); if (!libraries.length) { @@ -289,9 +366,9 @@ export class CoreH5PFramework { * @returns Promise resolved with the library data, rejected if not found. */ async getLibraryById(id: number, siteId?: string): Promise { - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); - const library = await db.getRecord(LIBRARIES_TABLE_NAME, { id }); + const library = await this.librariesTables[siteId].getOneByPrimaryKey({ id }); return this.parseLibDBData(library); } @@ -669,17 +746,14 @@ export class CoreH5PFramework { folderName: string, siteId?: string, ): Promise { - - const db = await CoreSites.getSiteDb(siteId); + const targetSiteId = siteId ?? CoreSites.getCurrentSiteId(); await Promise.all(Object.keys(dependencies).map(async (key) => { - const data: Partial = { + await this.librariesCachedAssetsTables[targetSiteId].insert({ hash: key, libraryid: dependencies[key].libraryId, foldername: folderName, - }; - - await db.insertRecord(LIBRARIES_CACHEDASSETS_TABLE_NAME, data); + }); })); } @@ -691,6 +765,8 @@ export class CoreH5PFramework { * @returns Promise resolved when done. */ async saveLibraryData(libraryData: CoreH5PLibraryBeingSaved, siteId?: string): Promise { + siteId ??= CoreSites.getCurrentSiteId(); + // Some special properties needs some checking and converting before they can be saved. const preloadedJS = this.libraryParameterValuesToCsv(libraryData, 'preloadedJs', 'path'); const preloadedCSS = this.libraryParameterValuesToCsv(libraryData, 'preloadedCss', 'path'); @@ -708,10 +784,7 @@ export class CoreH5PFramework { embedTypes = libraryData.embedTypes.join(', '); } - const site = await CoreSites.getSite(siteId); - - const db = site.getDb(); - const data: Partial = { + const data: Omit & Partial> = { title: libraryData.title, machinename: libraryData.machineName, majorversion: libraryData.majorVersion, @@ -733,16 +806,14 @@ export class CoreH5PFramework { data.id = libraryData.libraryId; } - await db.insertRecord(LIBRARIES_TABLE_NAME, data); + const libraryId = await this.librariesTables[siteId].insert(data); if (!data.id) { // New library. Get its ID. - const entry = await db.getRecord(LIBRARIES_TABLE_NAME, data); - - libraryData.libraryId = entry.id; + libraryData.libraryId = libraryId; } else { // Updated libary. Remove old dependencies. - await this.deleteLibraryDependencies(data.id, site.getId()); + await this.deleteLibraryDependencies(data.id, siteId); } } @@ -761,8 +832,7 @@ export class CoreH5PFramework { dependencyType: string, siteId?: string, ): Promise { - - const db = await CoreSites.getSiteDb(siteId); + const targetSiteId = siteId ?? CoreSites.getCurrentSiteId(); await Promise.all(dependencies.map(async (dependency) => { // Get the ID of the library. @@ -777,13 +847,15 @@ export class CoreH5PFramework { } // Create the relation. - const entry: Partial = { + if (typeof library.libraryId !== 'string') { + throw new CoreError('Attempted to create dependencies of library without id'); + } + + await this.libraryDependenciesTables[targetSiteId].insert({ libraryid: library.libraryId, requiredlibraryid: dependencyId, dependencytype: dependencyType, - }; - - await db.insertRecord(LIBRARY_DEPENDENCIES_TABLE_NAME, entry); + }); })); } @@ -800,8 +872,7 @@ export class CoreH5PFramework { librariesInUse: {[key: string]: CoreH5PContentDepsTreeDependency}, siteId?: string, ): Promise { - - const db = await CoreSites.getSiteDb(siteId); + const targetSiteId = siteId ?? CoreSites.getCurrentSiteId(); // Calculate the CSS to drop. const dropLibraryCssList: Record = {}; @@ -818,18 +889,17 @@ export class CoreH5PFramework { } } - // Now save the uusage. + // Now save the usage. await Promise.all(Object.keys(librariesInUse).map((key) => { const dependency = librariesInUse[key]; - const data: Partial = { + + return this.contentsLibrariesTables[targetSiteId].insert({ h5pid: id, libraryid: dependency.library.libraryId, dependencytype: dependency.type, dropcss: dropLibraryCssList[dependency.library.machineName] ? 1 : 0, - weight: dependency.weight, - }; - - return db.insertRecord(CONTENTS_LIBRARIES_TABLE_NAME, data); + weight: dependency.weight ?? 0, + }); })); } @@ -843,8 +913,7 @@ export class CoreH5PFramework { * @returns Promise resolved with content ID. */ async updateContent(content: CoreH5PContentBeingSaved, folderName: string, fileUrl: string, siteId?: string): Promise { - - const db = await CoreSites.getSiteDb(siteId); + siteId ??= CoreSites.getCurrentSiteId(); // If the libraryid declared in the package is empty, get the latest version. if (content.library && content.library.libraryId === undefined) { @@ -861,32 +930,31 @@ export class CoreH5PFramework { content.params = JSON.stringify(params); } - const data: Partial = { - id: undefined, - jsoncontent: content.params, + if (typeof content.library?.libraryId !== 'number') { + throw new CoreError('Attempted to create content of library without id'); + } + + const data: Omit & Partial> = { + jsoncontent: content.params ?? '{}', mainlibraryid: content.library?.libraryId, timemodified: Date.now(), filtered: null, foldername: folderName, fileurl: fileUrl, - timecreated: undefined, + timecreated: Date.now(), }; let contentId: number | undefined; if (content.id !== undefined) { data.id = content.id; contentId = content.id; - } else { - data.timecreated = data.timemodified; } - await db.insertRecord(CONTENT_TABLE_NAME, data); + const newContentId = await this.contentTables[siteId].insert(data); if (!contentId) { // New content. Get its ID. - const entry = await db.getRecord(CONTENT_TABLE_NAME, data); - - content.id = entry.id; + content.id = newContentId; contentId = content.id; } @@ -901,12 +969,9 @@ export class CoreH5PFramework { * @param siteId Site ID. If not defined, current site. */ async updateContentFields(id: number, fields: Partial, siteId?: string): Promise { + siteId ??= CoreSites.getCurrentSiteId(); - const db = await CoreSites.getSiteDb(siteId); - - const data = Object.assign({}, fields); - - await db.updateRecords(CONTENT_TABLE_NAME, data, { id }); + await this.contentTables[siteId].update(fields, { id }); } } From 26bf15496d6fff7afe53ef5275e23eb98b5fe0be Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 17 Jan 2024 17:09:18 +0100 Subject: [PATCH 09/19] MOBILE-4304 core: Remove getInOrEqual database helper --- src/core/classes/sites/site.ts | 9 +---- src/core/classes/sqlitedb.ts | 45 --------------------- src/core/features/course/services/course.ts | 8 +--- src/core/features/h5p/classes/framework.ts | 8 +--- src/core/services/filepool.ts | 19 +++------ 5 files changed, 12 insertions(+), 77 deletions(-) diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index da6267c7c97..9e695d78327 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -879,14 +879,9 @@ export class CoreSite extends CoreAuthenticatedSite { return await this.lastViewedTable.getMany({ component }); } - const whereAndParams = SQLiteDB.getInOrEqual(ids); - - whereAndParams.sql = 'id ' + whereAndParams.sql + ' AND component = ?'; - whereAndParams.params.push(component); - return await this.lastViewedTable.getManyWhere({ - sql: whereAndParams.sql, - sqlParams: whereAndParams.params, + sql: `id IN (${ids.map(() => '?').join(', ')}) AND component = ?`, + sqlParams: [...ids, component], js: (record) => record.component === component && ids.includes(record.id), }); } catch { diff --git a/src/core/classes/sqlitedb.ts b/src/core/classes/sqlitedb.ts index f6793f8cbbc..2e7637d0a4d 100644 --- a/src/core/classes/sqlitedb.ts +++ b/src/core/classes/sqlitedb.ts @@ -137,51 +137,6 @@ export interface SQLiteDBForeignKeySchema { */ export class SQLiteDB { - /** - * Constructs 'IN()' or '=' sql fragment - * - * @param items A single value or array of values for the expression. It doesn't accept objects. - * @param equal True means we want to equate to the constructed expression. - * @param onEmptyItems This defines the behavior when the array of items provided is empty. Defaults to false, - * meaning return empty. Other values will become part of the returned SQL fragment. - * @returns A list containing the constructed sql fragment and an array of parameters. - */ - static getInOrEqual( - items: SQLiteDBRecordValue | SQLiteDBRecordValue[], - equal: boolean = true, - onEmptyItems?: SQLiteDBRecordValue | null, - ): SQLiteDBQueryParams { - let sql = ''; - let params: SQLiteDBRecordValue[]; - - // Default behavior, return empty data on empty array. - if (Array.isArray(items) && !items.length && onEmptyItems === undefined) { - return { sql: '', params: [] }; - } - - // Handle onEmptyItems on empty array of items. - if (Array.isArray(items) && !items.length) { - if (onEmptyItems === null) { // Special case, NULL value. - sql = equal ? ' IS NULL' : ' IS NOT NULL'; - - return { sql, params: [] }; - } else { - items = [onEmptyItems as SQLiteDBRecordValue]; // Rest of cases, prepare items for processing. - } - } - - if (!Array.isArray(items) || items.length == 1) { - sql = equal ? '= ?' : '<> ?'; - params = Array.isArray(items) ? items : [items]; - } else { - const questionMarks = ',?'.repeat(items.length).substring(1); - sql = (equal ? '' : 'NOT ') + `IN (${questionMarks})`; - params = items; - } - - return { sql, params }; - } - db?: SQLiteObject; promise!: Promise; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index b2cb8d0c68d..23cea44b13f 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -56,7 +56,6 @@ import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CorePlatform } from '@services/platform'; import { asyncObservable } from '@/core/utils/rxjs'; import { firstValueFrom } from 'rxjs'; @@ -391,12 +390,9 @@ export class CoreCourseProvider { } const site = await CoreSites.getSite(siteId); - - const whereAndParams = SQLiteDB.getInOrEqual(ids); - const entries = await this.viewedModulesTables[site.getId()].getManyWhere({ - sql: 'cmId ' + whereAndParams.sql, - sqlParams: whereAndParams.params, + sql: `cmId IN (${ids.map(() => '?').join(', ')})`, + sqlParams: ids, js: (record) => ids.includes(record.cmId), }); diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index fb6c79080ea..6689fa20fa5 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -43,7 +43,6 @@ import { CoreH5PContentBeingSaved, CoreH5PLibraryBeingSaved } from './storage'; import { CoreH5PLibraryAddTo, CoreH5PLibraryMetadataSettings } from './validator'; import { CoreH5PMetadata } from './metadata'; import { Translate } from '@singletons'; -import { SQLiteDB } from '@classes/sqlitedb'; import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; import { LazyMap, lazyMap } from '@/core/utils/lazy-map'; import { CoreDatabaseTable } from '@classes/database/database-table'; @@ -138,14 +137,11 @@ export class CoreH5PFramework { siteId ??= CoreSites.getCurrentSiteId(); - const whereAndParams = SQLiteDB.getInOrEqual(libraryIds); - whereAndParams.sql = 'mainlibraryid ' + whereAndParams.sql; - await this.contentTables[siteId].updateWhere( { filtered: null }, { - sql: whereAndParams.sql, - sqlParams: whereAndParams.params, + sql: `mainlibraryid IN (${libraryIds.map(() => '?').join(', ')})`, + sqlParams: libraryIds, js: record => libraryIds.includes(record.mainlibraryid), }, ); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 2e4874e06ee..995b6c15834 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -28,7 +28,6 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; import { ApplicationInit, makeSingleton, NgZone, Translate } from '@singletons'; @@ -2270,6 +2269,8 @@ export class CoreFilepoolProvider { componentId?: string | number, onlyUnknown: boolean = true, ): Promise { + siteId = siteId ?? CoreSites.getCurrentSiteId(); + const items = await this.getComponentFiles(siteId, component, componentId); if (!items.length) { @@ -2277,23 +2278,15 @@ export class CoreFilepoolProvider { return; } - siteId = siteId ?? CoreSites.getCurrentSiteId(); - const fileIds = items.map((item) => item.fileId); - const whereAndParams = SQLiteDB.getInOrEqual(fileIds); - - whereAndParams.sql = 'fileId ' + whereAndParams.sql; - - if (onlyUnknown) { - whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL + ')'; - } - await this.filesTables[siteId].updateWhere( { stale: 1 }, { - sql: whereAndParams.sql, - sqlParams: whereAndParams.params, + sql: onlyUnknown + ? `fileId IN (${fileIds.map(() => '?').join(', ')}) AND (${CoreFilepoolProvider.FILE_IS_UNKNOWN_SQL})` + : `fileId IN (${fileIds.map(() => '?').join(', ')})`, + sqlParams: fileIds, js: record => fileIds.includes(record.fileId) && ( !onlyUnknown || CoreFilepoolProvider.FILE_IS_UNKNOWN_JS(record) ), From 368bf02bc22d0584f520fbc0fabf9f7aa5a09af9 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 18 Jan 2024 13:57:36 +0100 Subject: [PATCH 10/19] MOBILE-4304 h5p: Fix async delete --- src/core/features/h5p/classes/file-storage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/features/h5p/classes/file-storage.ts b/src/core/features/h5p/classes/file-storage.ts index 7cccc2b0d59..1be91e3c119 100644 --- a/src/core/features/h5p/classes/file-storage.ts +++ b/src/core/features/h5p/classes/file-storage.ts @@ -201,14 +201,14 @@ export class CoreH5PFileStorage { const result = await db.execute(query, queryArgs); - await Array.from(result.rows).map(async (entry: {foldername: string}) => { + await Promise.all(Array.from(result.rows).map(async (entry: {foldername: string}) => { try { // Delete the index.html. await this.deleteContentIndex(entry.foldername, site.getId()); } catch { // Ignore errors. } - }); + })); } /** From a7bd1e5f89a35d454cbb1a943714cb59a37bb26f Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 1 Feb 2024 16:14:53 +0100 Subject: [PATCH 11/19] MOBILE-4304 core: Replace WebSQL with sqlite-wasm --- angular.json | 6 +- package-lock.json | 9 + package.json | 3 +- ...sqlite.org+sqlite-wasm+3.45.0-build1.patch | 31 +++ scripts/copy-assets.js | 2 + src/core/classes/sqlitedb.ts | 132 +---------- .../features/emulator/classes/sqlitedb.ts | 219 ------------------ .../emulator/classes/wasm-sqlite-object.ts | 130 +++++++++++ src/core/features/emulator/emulator.module.ts | 6 + src/core/features/emulator/services/db.ts | 47 ++++ src/core/services/db.ts | 133 +++++++++-- src/types/sqlite-wasm.d.ts | 93 ++++++++ 12 files changed, 440 insertions(+), 371 deletions(-) create mode 100644 patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch delete mode 100644 src/core/features/emulator/classes/sqlitedb.ts create mode 100644 src/core/features/emulator/classes/wasm-sqlite-object.ts create mode 100644 src/core/features/emulator/services/db.ts create mode 100644 src/types/sqlite-wasm.d.ts diff --git a/angular.json b/angular.json index cbffd58ac33..07faf01a018 100644 --- a/angular.json +++ b/angular.json @@ -95,7 +95,11 @@ "options": { "disableHostCheck": true, "port": 8100, - "buildTarget": "app:build" + "buildTarget": "app:build", + "headers": { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" + } }, "configurations": { "production": { diff --git a/package-lock.json b/package-lock.json index 41ee2d71951..18ad9436560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", + "@sqlite.org/sqlite-wasm": "^3.45.0-build1", "@types/chart.js": "^2.9.31", "@types/cordova": "0.0.34", "@types/dom-mediacapture-record": "1.0.7", @@ -8997,6 +8998,14 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.45.0-build1", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.45.0-build1.tgz", + "integrity": "sha512-QAwE4n16t82g8kbhpuBzy6pzh7bm5VKziNKwQHmIPmtCBUk2AlUndsGS5qL8pAfOrrafXq9xILa0LdZkPFetgA==", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, "node_modules/@stencil/core": { "version": "4.10.0", "license": "MIT", diff --git a/package.json b/package.json index 440f6af0424..a04ed7d4b36 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ], "scripts": { "ng": "ng", - "start": "ionic serve --browser=$MOODLE_APP_BROWSER", + "start": "ionic serve --browser=$MOODLE_APP_BROWSER --ssl", "serve:test": "NODE_ENV=testing ionic serve --no-open", "build": "ionic build", "build:prod": "NODE_ENV=production ionic build --prod", @@ -89,6 +89,7 @@ "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", + "@sqlite.org/sqlite-wasm": "^3.45.0-build1", "@types/chart.js": "^2.9.31", "@types/cordova": "0.0.34", "@types/dom-mediacapture-record": "1.0.7", diff --git a/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch b/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch new file mode 100644 index 00000000000..793eae2e005 --- /dev/null +++ b/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch @@ -0,0 +1,31 @@ +diff --git a/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs b/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +index b86a0aa..a9bf793 100644 +--- a/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs ++++ b/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +@@ -533,7 +533,7 @@ var sqlite3InitModule = (() => { + wasmBinaryFile = locateFile(wasmBinaryFile); + } + } else { +- wasmBinaryFile = new URL('sqlite3.wasm', import.meta.url).href; ++ wasmBinaryFile = '/assets/lib/sqlite3/sqlite3.wasm'; + } + + function getBinary(file) { +@@ -12522,7 +12522,7 @@ var sqlite3InitModule = (() => { + return promiseResolve_(sqlite3); + }; + const W = new Worker( +- new URL('sqlite3-opfs-async-proxy.js', import.meta.url), ++ '/assets/lib/sqlite3/sqlite3-opfs-async-proxy.js', + ); + setTimeout(() => { + if (undefined === promiseWasRejected) { +@@ -13445,7 +13445,7 @@ var sqlite3InitModule = (() => { + }); + return thePromise; + }; +- installOpfsVfs.defaultProxyUri = 'sqlite3-opfs-async-proxy.js'; ++ installOpfsVfs.defaultProxyUri = '/assets/lib/sqlite3/sqlite3-opfs-async-proxy.js'; + globalThis.sqlite3ApiBootstrap.initializersAsync.push( + async (sqlite3) => { + try { diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js index 72274160b9d..5e9bf9dd28a 100644 --- a/scripts/copy-assets.js +++ b/scripts/copy-assets.js @@ -31,6 +31,8 @@ const ASSETS = { '/src/core/features/h5p/assets': '/lib/h5p', '/node_modules/ogv/dist': '/lib/ogv', '/node_modules/video.js/dist/video-js.min.css': '/lib/video.js/video-js.min.css', + '/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm': '/lib/sqlite3/sqlite3.wasm', + '/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js': '/lib/sqlite3/sqlite3-opfs-async-proxy.js', }; module.exports = function(ctx) { diff --git a/src/core/classes/sqlitedb.ts b/src/core/classes/sqlitedb.ts index 2e7637d0a4d..5bb9c40ec3f 100644 --- a/src/core/classes/sqlitedb.ts +++ b/src/core/classes/sqlitedb.ts @@ -14,10 +14,7 @@ import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; -import { SQLite } from '@singletons'; import { CoreError } from '@classes/errors/error'; -import { CoreDB } from '@services/db'; -import { CorePlatform } from '@services/platform'; type SQLiteDBColumnType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB'; @@ -137,17 +134,13 @@ export interface SQLiteDBForeignKeySchema { */ export class SQLiteDB { - db?: SQLiteObject; - promise!: Promise; - /** * Create and open the database. * * @param name Database name. + * @param db Database connection. */ - constructor(public name: string) { - this.init(); - } + constructor(public name: string, private db: SQLiteObject) {} /** * Add a column to an existing table. @@ -277,9 +270,7 @@ export class SQLiteDB { * @returns Promise resolved when done. */ async close(): Promise { - await this.ready(); - - await this.db?.close(); + await this.db.close(); } /** @@ -455,9 +446,7 @@ export class SQLiteDB { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise { - await this.ready(); - - return this.db?.executeSql(sql, params); + return this.db.executeSql(sql, params); } /** @@ -470,9 +459,7 @@ export class SQLiteDB { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async executeBatch(sqlStatements: (string | string[] | any)[]): Promise { - await this.ready(); - - await this.db?.sqlBatch(sqlStatements); + await this.db.sqlBatch(sqlStatements); } /** @@ -753,25 +740,6 @@ export class SQLiteDB { }; } - /** - * Initialize the database. - */ - init(): void { - this.promise = this.createDatabase().then(db => { - if (CoreDB.loggingEnabled()) { - const spies = this.getDatabaseSpies(db); - - db = new Proxy(db, { - get: (target, property, receiver) => spies[property] ?? Reflect.get(target, property, receiver), - }); - } - - this.db = db; - - return; - }); - } - /** * Insert a record into a table and return the "rowId" field. * @@ -898,18 +866,7 @@ export class SQLiteDB { * @returns Promise resolved when open. */ async open(): Promise { - await this.ready(); - - await this.db?.open(); - } - - /** - * Wait for the DB to be ready. - * - * @returns Promise resolved when ready. - */ - ready(): Promise { - return this.promise; + await this.db.open(); } /** @@ -1094,83 +1051,6 @@ export class SQLiteDB { return { sql, params }; } - /** - * Open a database connection. - * - * @returns Database. - */ - protected async createDatabase(): Promise { - await CorePlatform.ready(); - - return SQLite.create({ name: this.name, location: 'default' }); - } - - /** - * Get database spy methods to intercept database calls and track logging information. - * - * @param db Database to spy. - * @returns Spy methods. - */ - protected getDatabaseSpies(db: SQLiteObject): Partial { - const dbName = this.name; - - return { - async executeSql(statement, params) { - const start = performance.now(); - - try { - const result = await db.executeSql(statement, params); - - CoreDB.logQuery({ - params, - sql: statement, - duration: performance.now() - start, - dbName, - }); - - return result; - } catch (error) { - CoreDB.logQuery({ - params, - error, - sql: statement, - duration: performance.now() - start, - dbName, - }); - - throw error; - } - }, - async sqlBatch(statements) { - const start = performance.now(); - const sql = Array.isArray(statements) - ? statements.join(' | ') - : String(statements); - - try { - const result = await db.sqlBatch(statements); - - CoreDB.logQuery({ - sql, - duration: performance.now() - start, - dbName, - }); - - return result; - } catch (error) { - CoreDB.logQuery({ - sql, - error, - duration: performance.now() - start, - dbName, - }); - - throw error; - } - }, - }; - } - } export type SQLiteDBRecordValues = { diff --git a/src/core/features/emulator/classes/sqlitedb.ts b/src/core/features/emulator/classes/sqlitedb.ts deleted file mode 100644 index f0243ba6f65..00000000000 --- a/src/core/features/emulator/classes/sqlitedb.ts +++ /dev/null @@ -1,219 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { SQLiteDB } from '@classes/sqlitedb'; -import { DbTransaction, SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; -import { CoreDB } from '@services/db'; - -/** - * Class to mock the interaction with the SQLite database. - */ -export class SQLiteDBMock extends SQLiteDB { - - /** - * Create and open the database. - * - * @param name Database name. - */ - constructor(public name: string) { - super(name); - } - - /** - * Close the database. - * - * @returns Promise resolved when done. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - close(): Promise { - // WebSQL databases aren't closed. - return Promise.resolve(); - } - - /** - * Drop all the data in the database. - * - * @returns Promise resolved when done. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async emptyDatabase(): Promise { - await this.ready(); - - return new Promise((resolve, reject): void => { - this.db?.transaction((tx) => { - // Query all tables from sqlite_master that we have created and can modify. - const args = []; - const query = `SELECT * FROM sqlite_master - WHERE name NOT LIKE 'sqlite\\_%' escape '\\' AND name NOT LIKE '\\_%' escape '\\'`; - - tx.executeSql(query, args, (tx, result) => { - if (result.rows.length <= 0) { - // No tables to delete, stop. - resolve(null); - - return; - } - - // Drop all the tables. - const promises: Promise[] = []; - - for (let i = 0; i < result.rows.length; i++) { - promises.push(new Promise((resolve, reject): void => { - // Drop the table. - const name = JSON.stringify(result.rows.item(i).name); - tx.executeSql('DROP TABLE ' + name, [], resolve, reject); - })); - } - - Promise.all(promises).then(resolve).catch(reject); - }, reject); - }); - }); - } - - /** - * Execute a SQL query. - * IMPORTANT: Use this function only if you cannot use any of the other functions in this API. Please take into account that - * these query will be run in SQLite (Mobile) and Web SQL (desktop), so your query should work in both environments. - * - * @param sql SQL query to execute. - * @param params Query parameters. - * @returns Promise resolved with the result. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async execute(sql: string, params?: any[]): Promise { - await this.ready(); - - return new Promise((resolve, reject): void => { - // With WebSQL, all queries must be run in a transaction. - this.db?.transaction((tx) => { - tx.executeSql( - sql, - params, - (_, results) => resolve(results), - (_, error) => reject(new Error(`SQL failed: ${sql}, reason: ${error?.message}`)), - ); - }); - }); - } - - /** - * Execute a set of SQL queries. This operation is atomic. - * IMPORTANT: Use this function only if you cannot use any of the other functions in this API. Please take into account that - * these query will be run in SQLite (Mobile) and Web SQL (desktop), so your query should work in both environments. - * - * @param sqlStatements SQL statements to execute. - * @returns Promise resolved with the result. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async executeBatch(sqlStatements: any[]): Promise { - await this.ready(); - - return new Promise((resolve, reject): void => { - // Create a transaction to execute the queries. - this.db?.transaction((tx) => { - const promises: Promise[] = []; - - // Execute all the queries. Each statement can be a string or an array. - sqlStatements.forEach((statement) => { - promises.push(new Promise((resolve, reject): void => { - let query; - let params; - - if (Array.isArray(statement)) { - query = statement[0]; - params = statement[1]; - } else { - query = statement; - params = null; - } - - tx.executeSql(query, params, (_, results) => resolve(results), (_, error) => reject(error)); - })); - }); - - // eslint-disable-next-line promise/catch-or-return - Promise.all(promises).then(resolve, reject); - }); - }); - } - - /** - * Open the database. Only needed if it was closed before, a database is automatically opened when created. - * - * @returns Promise resolved when done. - */ - open(): Promise { - // WebSQL databases can't closed, so the open method isn't needed. - return Promise.resolve(); - } - - /** - * @inheritdoc - */ - protected async createDatabase(): Promise { - // This DB is for desktop apps, so use a big size to be sure it isn't filled. - return (window as unknown as WebSQLWindow).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); - } - - /** - * @inheritdoc - */ - protected getDatabaseSpies(db: SQLiteObject): Partial { - const dbName = this.name; - - return { - transaction: (callback) => db.transaction((transaction) => { - const transactionSpy: DbTransaction = { - executeSql(sql, params, success, error) { - const start = performance.now(); - - return transaction.executeSql( - sql, - params, - (...args) => { - CoreDB.logQuery({ - sql, - params, - duration: performance.now() - start, - dbName, - }); - - return success?.(...args); - }, - (...args) => { - CoreDB.logQuery({ - sql, - params, - error: args[0], - duration: performance.now() - start, - dbName, - }); - - return error?.(...args); - }, - ); - }, - }; - - return callback(transactionSpy); - }), - }; - } - -} - -interface WebSQLWindow extends Window { - openDatabase(name: string, version: string, displayName: string, estimatedSize: number): SQLiteObject; -} diff --git a/src/core/features/emulator/classes/wasm-sqlite-object.ts b/src/core/features/emulator/classes/wasm-sqlite-object.ts new file mode 100644 index 00000000000..e76fb54e45d --- /dev/null +++ b/src/core/features/emulator/classes/wasm-sqlite-object.ts @@ -0,0 +1,130 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; +import { CorePromisedValue } from '@classes/promised-value'; +import { Sqlite3Worker1Promiser, sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm'; + +/** + * Throw an error indicating that the given method hasn't been implemented. + * + * @param method Method name. + */ +function notImplemented(method: string): any { + throw new Error(`${method} method not implemented.`); +} + +/** + * SQLiteObject adapter implemented using the sqlite-wasm package. + */ +export class WasmSQLiteObject implements SQLiteObject { + + private name: string; + private promisedPromiser: CorePromisedValue; + private promiser: Sqlite3Worker1Promiser; + + constructor(name: string) { + this.name = name; + this.promisedPromiser = new CorePromisedValue(); + this.promiser = async (...args) => { + const promiser = await this.promisedPromiser; + + return promiser.call(promiser, ...args); + }; + } + + /** + * Delete the database. + */ + async delete(): Promise { + if (!this.promisedPromiser.isResolved()) { + await this.open(); + } + + await this.promiser('close', { unlink: true }); + } + + /** + * @inheritdoc + */ + async open(): Promise { + const promiser = await new Promise((resolve) => { + const _promiser = sqlite3Worker1Promiser(() => resolve(_promiser)); + }); + + await promiser('open', { filename: `file:${this.name}.sqlite3`, vfs: 'opfs' }); + + this.promisedPromiser.resolve(promiser); + } + + /** + * @inheritdoc + */ + async close(): Promise { + await this.promiser('close', {}); + } + + /** + * @inheritdoc + */ + async executeSql(statement: string, params?: any[] | undefined): Promise { + const rows = [] as unknown[]; + + await this.promiser('exec', { + sql: statement, + bind: params, + callback({ row, columnNames }) { + if (!row) { + return; + } + + rows.push(columnNames.reduce((record, column, index) => { + record[column] = row[index]; + + return record; + }, {})); + }, + }); + + return { + rows: { + item: (i: number) => rows[i], + length: rows.length, + }, + rowsAffected: rows.length, + }; + } + + /** + * @inheritdoc + */ + async sqlBatch(sqlStatements: any[]): Promise { + await Promise.all(sqlStatements.map(sql => this.executeSql(sql))); + } + + // These methods and properties are not used in our app, + // but still need to be declared to conform with the SQLiteObject interface. + _objectInstance = null; // eslint-disable-line @typescript-eslint/naming-convention + databaseFeatures = { isSQLitePluginDatabase: false }; + openDBs = null; + addTransaction = () => notImplemented('SQLiteObject.addTransaction'); + transaction = () => notImplemented('SQLiteObject.transaction'); + readTransaction = () => notImplemented('SQLiteObject.readTransaction'); + startNextTransaction = () => notImplemented('SQLiteObject.startNextTransaction'); + abortallPendingTransactions = () => notImplemented('SQLiteObject.abortallPendingTransactions'); + +} diff --git a/src/core/features/emulator/emulator.module.ts b/src/core/features/emulator/emulator.module.ts index b40e57b731b..098ef3e9e3c 100644 --- a/src/core/features/emulator/emulator.module.ts +++ b/src/core/features/emulator/emulator.module.ts @@ -42,6 +42,8 @@ import { CorePlatform } from '@services/platform'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreNative } from '@features/native/services/native'; import { SecureStorageMock } from '@features/emulator/classes/SecureStorage'; +import { CoreDbProvider } from '@services/db'; +import { CoreDbProviderMock } from '@features/emulator/services/db'; /** * This module handles the emulation of Cordova plugins in browser and desktop. @@ -95,6 +97,10 @@ import { SecureStorageMock } from '@features/emulator/classes/SecureStorage'; ? new LocalNotifications() : new LocalNotificationsMock(), }, + { + provide: CoreDbProvider, + useFactory: (): CoreDbProvider => CorePlatform.is('cordova') ? new CoreDbProvider() : new CoreDbProviderMock(), + }, { provide: APP_INITIALIZER, useValue: async () => { diff --git a/src/core/features/emulator/services/db.ts b/src/core/features/emulator/services/db.ts new file mode 100644 index 00000000000..44a9947c041 --- /dev/null +++ b/src/core/features/emulator/services/db.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { asyncInstance } from '@/core/utils/async-instance'; +import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; +import { WasmSQLiteObject } from '@features/emulator/classes/wasm-sqlite-object'; +import { CoreDbProvider } from '@services/db'; + +/** + * Emulates the database provider in the browser. + */ +export class CoreDbProviderMock extends CoreDbProvider { + + /** + * @inheritdoc + */ + protected createDatabase(name: string): SQLiteObject { + return asyncInstance(async () => { + const db = new WasmSQLiteObject(name); + + await db.open(); + + return db; + }); + } + + /** + * @inheritdoc + */ + protected async deleteDatabase(name: string): Promise { + const db = new WasmSQLiteObject(name); + + await db.delete(); + } + +} diff --git a/src/core/services/db.ts b/src/core/services/db.ts index 87eefd5193f..80df9a09f7d 100644 --- a/src/core/services/db.ts +++ b/src/core/services/db.ts @@ -15,10 +15,11 @@ import { Injectable } from '@angular/core'; import { SQLiteDB } from '@classes/sqlitedb'; -import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; import { CoreBrowser } from '@singletons/browser'; -import { makeSingleton, SQLite } from '@singletons'; +import { SQLite, makeSingleton } from '@singletons'; import { CorePlatform } from '@services/platform'; +import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; +import { asyncInstance } from '@/core/utils/async-instance'; const tableNameRegex = new RegExp([ '^SELECT.*FROM ([^ ]+)', @@ -208,45 +209,129 @@ export class CoreDbProvider { */ getDB(name: string, forceNew?: boolean): SQLiteDB { if (this.dbInstances[name] === undefined || forceNew) { - if (CorePlatform.is('cordova')) { - this.dbInstances[name] = new SQLiteDB(name); - } else { - this.dbInstances[name] = new SQLiteDBMock(name); + let db = this.createDatabase(name); + + if (this.loggingEnabled()) { + const spies = this.getDatabaseSpies(name, db); + + db = new Proxy(db, { + get: (target, property, receiver) => spies[property] ?? Reflect.get(target, property, receiver), + }) as unknown as SQLiteObject; } + + this.dbInstances[name] = new SQLiteDB(name, db); } return this.dbInstances[name]; } + /** + * Create database connection. + * + * @param name Database name. + * @returns Database connection. + */ + protected createDatabase(name: string): SQLiteObject { + // Ideally, this method would return a Promise instead of resorting to Duck typing; + // but doing so would mean that the getDB() method should also return a promise. + // Given that it is heavily used throughout the app, we want to avoid it for now. + return asyncInstance(async () => { + await CorePlatform.ready(); + + return SQLite.create({ name, location: 'default' }); + }); + } + /** * Delete a DB. * * @param name DB name. - * @returns Promise resolved when the DB is deleted. */ async deleteDB(name: string): Promise { if (this.dbInstances[name] !== undefined) { - // Close the database first. await this.dbInstances[name].close(); - const db = this.dbInstances[name]; delete this.dbInstances[name]; - - if (db instanceof SQLiteDBMock) { - // In WebSQL we cannot delete the database, just empty it. - return db.emptyDatabase(); - } else { - return SQLite.deleteDatabase({ - name, - location: 'default', - }); - } - } else if (CorePlatform.is('cordova')) { - return SQLite.deleteDatabase({ - name, - location: 'default', - }); } + + await this.deleteDatabase(name); + } + + /** + * Delete database. + * + * @param name Database name. + */ + protected async deleteDatabase(name: string): Promise { + await SQLite.deleteDatabase({ + name, + location: 'default', + }); + } + + /** + * Get database spy methods to intercept database calls and track logging information. + * + * @param dbName Database name. + * @param db Database to spy. + * @returns Spy methods. + */ + protected getDatabaseSpies(dbName: string, db: SQLiteObject): Partial { + return { + async executeSql(statement, params) { + const start = performance.now(); + + try { + const result = await db.executeSql(statement, params); + + CoreDB.logQuery({ + params, + sql: statement, + duration: performance.now() - start, + dbName, + }); + + return result; + } catch (error) { + CoreDB.logQuery({ + params, + error, + sql: statement, + duration: performance.now() - start, + dbName, + }); + + throw error; + } + }, + async sqlBatch(statements) { + const start = performance.now(); + const sql = Array.isArray(statements) + ? statements.join(' | ') + : String(statements); + + try { + const result = await db.sqlBatch(statements); + + CoreDB.logQuery({ + sql, + duration: performance.now() - start, + dbName, + }); + + return result; + } catch (error) { + CoreDB.logQuery({ + sql, + error, + duration: performance.now() - start, + dbName, + }); + + throw error; + } + }, + }; } } diff --git a/src/types/sqlite-wasm.d.ts b/src/types/sqlite-wasm.d.ts new file mode 100644 index 00000000000..2e89bf70dbb --- /dev/null +++ b/src/types/sqlite-wasm.d.ts @@ -0,0 +1,93 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Brand } from '@/core/utils/types'; + +// Can be removed when the following issue is fixed: +// https://github.com/sqlite/sqlite-wasm/issues/53 + +declare module '@sqlite.org/sqlite-wasm' { + + export type SqliteDbId = Brand; + + export interface SqliteRowData { + columnNames: string[]; + row: SqlValue[] | undefined; + rowNumber: number | null; + } + + export interface Sqlite3Worker1Messages { + close: { + args?: { + unlink?: boolean; + }; + result: { + filename?: string; + }; + }; + 'config-get': { + result: { + version: object; + bigIntEnabled: boolean; + vfsList: unknown; + }; + }; + exec: { + args: { + sql: string; + bind?: BindingSpec; + callback?(data: SqliteRowData): void | false; + }; + }; + open: { + args: { + filename: string; + vfs?: string; + }; + result: { + dbId: SqliteDbId; + filename: string; + persistent: boolean; + vfs: string; + }; + }; + } + + export interface Sqlite3Worker1PromiserConfig { + onready(): void; + worker?: unknown; + generateMessageId?(message: object): string; + debug?(...args: unknown[]): void; + onunhandled?(event: unknown): void; + } + + export type Sqlite3Worker1PromiserMethodOptions = + Sqlite3Worker1Messages[T] extends { args?: infer TArgs } + ? { type: T; args: TArgs } + : { type: T; args?: Sqlite3Worker1Messages[T]['args'] }; + + export type Sqlite3Worker1Promiser = + (( + type: T, + args: Sqlite3Worker1Messages[T]['args'], + ) => Promise) & + (( + options: Sqlite3Worker1PromiserMethodOptions, + ) => Promise); + + export function sqlite3Worker1Promiser(): Sqlite3Worker1Promiser; + export function sqlite3Worker1Promiser(onready: () => void): Sqlite3Worker1Promiser; + export function sqlite3Worker1Promiser(config: Sqlite3Worker1PromiserOptions): Sqlite3Worker1Promiser; + +} From ed75657719338d86130a8716cfa2bf6a02fe1d8b Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 1 Feb 2024 16:22:16 +0100 Subject: [PATCH 12/19] MOBILE-4304 core: Implement insert row id --- ...sqlite.org+sqlite-wasm+3.45.0-build1.patch | 32 +++++++++++++++++-- .../emulator/classes/wasm-sqlite-object.ts | 6 +++- src/types/sqlite-wasm-custom.d.ts | 23 +++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/types/sqlite-wasm-custom.d.ts diff --git a/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch b/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch index 793eae2e005..a91134f24fd 100644 --- a/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch +++ b/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs b/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs -index b86a0aa..a9bf793 100644 +index b86a0aa..1be2b82 100644 --- a/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +++ b/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs @@ -533,7 +533,7 @@ var sqlite3InitModule = (() => { @@ -11,7 +11,33 @@ index b86a0aa..a9bf793 100644 } function getBinary(file) { -@@ -12522,7 +12522,7 @@ var sqlite3InitModule = (() => { +@@ -10913,6 +10913,10 @@ var sqlite3InitModule = (() => { + } + }, + ++ lastInsertRowId: function () { ++ return capi.sqlite3_last_insert_rowid(affirmDbOpen(this).pointer); ++ }, ++ + dbFilename: function (dbName = 'main') { + return capi.sqlite3_db_filename(affirmDbOpen(this).pointer, dbName); + }, +@@ -11877,12 +11881,14 @@ var sqlite3InitModule = (() => { + if (!hadColNames) rc.columnNames = []; + + rc.callback = function (row, stmt) { ++ const rowId = rc.sql.includes('INSERT') ? db.lastInsertRowId() : undefined; + wState.post( + { + type: theCallback, + columnNames: rc.columnNames, + rowNumber: ++rowNumber, + row: row, ++ rowId, + }, + wState.xfer, + ); +@@ -12522,7 +12528,7 @@ var sqlite3InitModule = (() => { return promiseResolve_(sqlite3); }; const W = new Worker( @@ -20,7 +46,7 @@ index b86a0aa..a9bf793 100644 ); setTimeout(() => { if (undefined === promiseWasRejected) { -@@ -13445,7 +13445,7 @@ var sqlite3InitModule = (() => { +@@ -13445,7 +13451,7 @@ var sqlite3InitModule = (() => { }); return thePromise; }; diff --git a/src/core/features/emulator/classes/wasm-sqlite-object.ts b/src/core/features/emulator/classes/wasm-sqlite-object.ts index e76fb54e45d..8dc35cddd49 100644 --- a/src/core/features/emulator/classes/wasm-sqlite-object.ts +++ b/src/core/features/emulator/classes/wasm-sqlite-object.ts @@ -82,16 +82,19 @@ export class WasmSQLiteObject implements SQLiteObject { * @inheritdoc */ async executeSql(statement: string, params?: any[] | undefined): Promise { + let insertId: number | undefined = undefined; const rows = [] as unknown[]; await this.promiser('exec', { sql: statement, bind: params, - callback({ row, columnNames }) { + callback({ row, columnNames, rowId }) { if (!row) { return; } + insertId ||= rowId; + rows.push(columnNames.reduce((record, column, index) => { record[column] = row[index]; @@ -106,6 +109,7 @@ export class WasmSQLiteObject implements SQLiteObject { length: rows.length, }, rowsAffected: rows.length, + insertId, }; } diff --git a/src/types/sqlite-wasm-custom.d.ts b/src/types/sqlite-wasm-custom.d.ts new file mode 100644 index 00000000000..d3151021a06 --- /dev/null +++ b/src/types/sqlite-wasm-custom.d.ts @@ -0,0 +1,23 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export {}; + +declare module '@sqlite.org/sqlite-wasm' { + + export interface SqliteRowData { + rowId?: number; + } + +} From c0272de73193ddc1b7c366f805c11c68eb84f0e1 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 1 Feb 2024 17:04:40 +0100 Subject: [PATCH 13/19] MOBILE-4304 ci: Configure SSL --- .github/workflows/acceptance.yml | 34 +++++++++++++++++++++++++------- Dockerfile | 9 ++++++++- nginx.conf | 16 ++++++++++++++- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 9cc0288ca93..d30a6611f5b 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -41,6 +41,17 @@ jobs: working-directory: app run: npm run build:test + - name: Generate SSL certificates + working-directory: app + run: | + mkdir ./ssl + openssl req -x509 -nodes \ + -days 365 \ + -newkey rsa:2048 \ + -keyout ./ssl/certificate.key \ + -out ./ssl/certificate.crt \ + -subj="/O=Moodle" + - name: Build Behat plugin working-directory: app run: ./scripts/build-behat-plugin.js ../plugin @@ -111,11 +122,12 @@ jobs: - uses: actions/cache/save@v4 with: - key: build-${{ github.sha }} - path: | - app/node_modules/**/* - app/www/**/* - plugin/**/* + key: build-${{ github.sha }} + path: | + app/ssl/**/* + app/node_modules/**/* + app/www/**/* + plugin/**/* behat: runs-on: ubuntu-latest @@ -157,6 +169,7 @@ jobs: with: key: build-${{ github.sha }} path: | + app/ssl/**/* app/node_modules/**/* app/www/**/* plugin/**/* @@ -164,7 +177,14 @@ jobs: - name: Launch Docker images working-directory: app run: | - docker run -d --rm -p 8001:80 --name moodleapp -v ./www:/usr/share/nginx/html -v ./nginx.conf:/etc/nginx/conf.d/default.conf nginx:alpine + docker run -d --rm \ + -p 8001:443 \ + --name moodleapp \ + -v ./www:/usr/share/nginx/html \ + -v ./nginx.conf:/etc/nginx/conf.d/default.conf \ + -v ./ssl/certificate.crt:/etc/ssl/certificate.crt \ + -v ./ssl/certificate.key:/etc/ssl/certificate.key \ + nginx:alpine docker run -d --rm -p 8002:80 --name bigbluebutton moodlehq/bigbluebutton_mock:latest - name: Initialise moodle-plugin-ci @@ -184,7 +204,7 @@ jobs: DB: pgsql MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }} MOODLE_REPO: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle.git' }} - MOODLE_BEHAT_IONIC_WWWROOT: http://localhost:8001 + MOODLE_BEHAT_IONIC_WWWROOT: https://localhost:8001 MOODLE_BEHAT_DEFAULT_BROWSER: chrome - name: Update config diff --git a/Dockerfile b/Dockerfile index a3f527cee8c..0a247812e56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,17 @@ ARG build_command="npm run build:prod" COPY . /app RUN ${build_command} +# Generate SSL certificate +RUN mkdir /app/ssl +RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /app/ssl/certificate.key -out /app/ssl/certificate.crt -subj="/O=Moodle" + ## SERVE STAGE FROM nginx:alpine as serve-stage # Copy assets & config COPY --from=build-stage /app/www /usr/share/nginx/html +COPY --from=build-stage /app/ssl/certificate.crt /etc/ssl/certificate.crt +COPY --from=build-stage /app/ssl/certificate.key /etc/ssl/certificate.key COPY ./nginx.conf /etc/nginx/conf.d/default.conf -HEALTHCHECK --interval=10s --timeout=4s CMD curl -f http://localhost/assets/env.json || exit 1 +EXPOSE 443 +HEALTHCHECK --interval=10s --timeout=4s CMD curl --insecure -f https://localhost/assets/env.json || exit 1 diff --git a/nginx.conf b/nginx.conf index 498543c33a4..3de153c8798 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,9 +1,23 @@ server { - listen 0.0.0.0:80; + listen 80; + listen 443 ssl; root /usr/share/nginx/html; server_tokens off; access_log off; + # Configure SSL + if ($scheme = "http") { + return 301 https://$host$request_uri; + } + + ssl_certificate /etc/ssl/certificate.crt; + ssl_certificate_key /etc/ssl/certificate.key; + ssl_protocols TLSv1.3; + + # Enable OPFS + add_header Cross-Origin-Opener-Policy "same-origin"; + add_header Cross-Origin-Embedder-Policy "require-corp"; + location / { try_files $uri $uri/ /index.html; } From 060256c0ea4e8e5a768da5efbec31b48f9f02825 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 6 Feb 2024 13:00:08 +0100 Subject: [PATCH 14/19] MOBILE-4304 behat: Update snapshots --- ...-forum-activity-in-app-reply-a-post_14.png | Bin 22587 -> 22900 bytes ...ubmit-a-quiz--review-a-quiz-attempt_26.png | Bin 21376 -> 21837 bytes ...ubmit-a-quiz--review-a-quiz-attempt_38.png | Bin 37371 -> 37359 bytes ...displayed-when-adding-a-new-account_13.png | Bin 31743 -> 33486 bytes ...-displayed-when-adding-a-new-account_9.png | Bin 30745 -> 28547 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png index 75f0af1c49eab4cb4cdbcd1d1e272de5888baa83..2ce99523a58945023e991207082c039ea2f823ff 100644 GIT binary patch literal 22900 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 literal 22587 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 XDx2RLh(ugP;J>?a_hfTrj9&a-Lh636 diff --git a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_38.png b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_38.png index b4dc52877d4932ed22958bd39b0fc17f35f06c74..e017040c7059d3fe748ec73706c89ac99daf09b6 100644 GIT binary patch literal 37359 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

xVyUG{|Wyn7Fjf@ z1LGCTKBHM&Qp#@MN4GkBqB&Bv(b^TEyZK$+^I)XVJu4UCN$Y92@?IOp6GL9sab6%RbJssT_ELsFI0~wC>al^MK0+5~hPCMo<-O;>#|m&1 zP~EukVNUlJV0j9tjNa8~Rco<+VRW#u(E&LJcmy;Qh;CVL${N1Or+rf`U_eDgMBq{h zB|dt){37$^J&<79-~ocoT{mm6eySCPLfJpFwDjD_=z+kMD?q^hX>7y-T@VwOL_}G6 zNQ&P8iVCF&D2L#A4`;%JNjQzJH-=Hs-Me?M&biEf4SfpOPyNPF3REzqU_4Y325qr5 zzSqd0^wx!8INfe#K>KoKM5Cl+APBc_WgpvnD zMG;-bzKQzuEQ02zddUZmjgW(-unDsi7Dc-&Qp@@hXfaa~OtV&E%CV-3caU{~1c^`zWF=jG+?0u2y|6I3;b zEF@50oS&b!r3eeZar3GX0TJ)HtgJ!bH+iV?a2XjHL0UN&a{oIs!}oUiCM*%@K@CSo z$6HiUKm!W<{r4i3_tB$tdXJrU9^<}m1i(6XSXBYHVY|Po0yMTbl-a>^^78Tx#$Le! zuD&%Odi9#y+SnYcAyme;oWTd<_fWnq4`$1Wh=`P$13XhNwnB4oIIx>TUA=mBvq=^G z9S|!vYa?8`Nmc6@FF|}LnwFuV-@jdCqFGs6Pxoai0HXj5An2YH{$XK;vrQ53 zg3Z?7r}G_2QakhQeD0^k^s-(sI*c_md)b;9DO0skD>om;@<=@bT2By4$+Q$Hvgp`?C%v^_`4VStM$lbcgDvv(}h zjsC+60OGVv0(Y*Sy5fmTWqBb|c9aB`ALm<39|=|$4OE6~is!3M zCWSyxH z$KX(|tx{9$fD+N7}pa_$rJGLqwwUmfZ;g}Fi$FQ5yHk00~gFQV4zQXPw&>E z-ci#B*l*-ts_y6rW&bK^Gq+q|w+A~J$oa;J2|Ni2EX@5a-Q67OhP9544#@jA;HB@c zoxE0u$BSL(Zu@?rbw4_=cvNw~3|OymV8EA|Vw8Z0qOMutGhnTq$xou?qotRJ6BDgE z)!W!D9iM5N@>h6hR(kNt?QJH??6U)JI7U%(b#cw`7QsL2|k?`=?*es2V zg9-h=A4xA!;H3vD-CG{WdhBDPA2LccvF*_`(Ge;pN+U1d4T_(5Si;Awb}}Bz2o_69 zK&YFWP5MkM;Vc`rDo{uM0m{$9-46Lc}@bK!TyoIL7#u@s3-}Df& zT$?@BEcFWUmoM29JSJo>sOw5x4k-UeK(-M__&&1C+P^qp{*H&>mlApyYl#t^vt7oe ztfD5L{_45!HPXXZCl2dFp^(+s?D~{H;6A_kP&M|duK4ugBC5I(*VC8eJ74i zPBkmsGDy_u{Ga=O{!7T`|MZtQj7h-!gs6{pOFcdyY5bj=dkU!LX!%DrklSo?UBSTD z`ekI$fr{?F1htO@K$qr1^HB|F;IMUs0Fpu+?(iqL+Z4q}jJQxBzXQu21evo7d@{9l zbuxVA3-;?{e7CvHjFz<_ya^DQD!e@fVDsC!_dY^Kg6bbK2Xt>LKm{;RB!ghR1=gJe z6aP*y{`~0^MYJcA`(+$CX2a}Y<_W)lMc@_aIgn`^AhU(&0~YxV0lIRe4ao12P_QH~?`9!VoPh>vhUY z-e7=%kmCiH#=ykA1ALN98OT-XK##xr{wg4pg5L%Nb-Lt65a?MQH?<~QcVrNz8HAkS zV(T<(kPj|GVZ6Zz^a;`@IJREo8OYq1O?+l!vrRZk42GMO=e5-!gz&i?bAq9yDPMN@ zx4yNN6>17tW4n!sB(S>VfsPKMfe+xf@zS?2Eo3NgxWEumC}wN40~$SCE1G;JEv;Am z4h^KITo@1c<0U%TWPxV52wF~VE}9mINv_}tg15Koi8XUT(*XK6VY(RRvbpQCXr_D` zCKy3xf>Z87t)UO17I;=7`5i1^Cx{yv8O84dmH!-mAQ!|Hn89*Tqs)eK zsOxgU%=QjG#IDzL9h;aL?)&W_K=t8#V?xA*R#SuK;^M;e@L_YZAU}k&pQ;7eKrw*K zB@0OW0f?ReK|$=PFXpv^$pI4zhuz|fz?<|>;6jT7zeaUCTS1^Hv6@DYq?1br-RCC# zOK-6G0cBkerjg!0a~L$RQ(z+2J+SRr!HUdybb(Yh9M4IBI(jEjSs?WM*|RI40C2cR zVu2|3#i%=ViFp-V1YGAyU_1b=3^tEM{BM4T^)JI2AjcUC#XOLI-x|Y`K_&sr5}jP~ z7qDR%xBLJXOf`Tj4)P07r@krY(l9eKH~cPl+Ig9y^?2qGEKza*Mst4u22m1xQRxfb zSPS(?64r0|LeHXE=~}w;vea&gGv&>jHvvIGE@meXJ1C`pzw4^2!+?PSWV`!nZ)2>) ztE|kW)CL4s*h0M@het+)_Brs~dk!A}>B5;pK~BzI{$iraBL&z!xU+fykUi#|tAqGk zTU$_WF^t!bYq63X1cZgrDP^gLQ4DI1m)K%K(9$h-evr1KV`vy-aZ!~FJOG>A{i_%t z9DoBO8tSoLK=8OML$HqP1_q*`1ejS}Oe^av zVPQHrguoWVJ-!b&VKC$lrc2Nw4NecYmJ0d!_&~I{& zT+Q(CQSJ!T70U zp%KOeDJD1!JHCMC2zm7*D4%FZh)0w}5QRz{w`jocgKQ#RKE7_ycB%xU0h~vHf5&QX z`E@jtN-c=ZP76s6>_1mve?usY74{G!X3;$4}Pm@E%gN^p!$+#TWzmqeguQkkgq_t(1?DP(6fLOesS zk?+5?1Es{_`U%xlA@}#NBy47zJmm@)>`X40J35A?`TFQeUFR4GBIL{EOH#pl^W+{a z?dFQ5i_x}y9o+!MP$PcT`Wwfhw24m-Xto(#^wY0)B=UED`kdw;8lVkqWLrF&52ymR zr?d1zlzhJ4-j@{cw{p*cQbRI&r3s_sATh z>7PQ9>M@%vG3A-e9U|h#K62Dj-aZf`qelE|G>cMAHzE+NJW6^;=v%sX4;@9?#3QVHDIo{5}G8P#2f9D zXJEA|b_FZ!64}be@PX!cr9Ff5RhF#_9dgFX6VDksxvyk1r%$Cy9G9>;&ggQTho1c9LQN; zELk)aAHER*Pz5t?mA+uuS$T1z_%FZE@&WZEi<+2Uq50wVN=YLB1r`cCRA#5fj1{hB z?3M$#6Q-lSDm&;+i)(%0+L4Zb1XtckYq# zq?~x*18lIVxS>7O(5{BMN&jjycMqZ1LDTHXQ!@nZ1(`kF6Nf90$fb2s(NLlo`_VB_>Um6Vm_=n|U zLIz6UdxoK_Ln9+yUv>FYQ+0Wr7|w!1Ws`*J3RZYF*N%k%E)|#Z?zsEeuZ~n;!Ed=- zkcnk}sCUZy&ci)0P=2Q9210k_lC|Z@++_LdDrS8_DXs1MRHHf~OMU6HB$nVjEYtz*{_ipBnINohi5fnw6|#1E%K z&O8jXi4?YO>*Y1D=Y-xFPWCWenaQ;=`E&9_TYt+#2$cOgk zfQ=ZIHpicsqNPGNZV+1A*tqJ98jn0bp1eFlvm@%NGa2UVn^LqfdqOq1bChxSfkG)Q z>dX(fGhYyR3Zd2RD8=>!f!?8~Fw|Qx)Wr*Jk0B(glsI2|{@j?Q0~{i08xkow7uAoq z=bL4urI~WAi$X@THE=+#qZ#K}%h(&7tePx;i{o%WZrt*_tmLnjWv7o+%*9Kd#9-)f z@#Ejx0$0K9*7ZrbJNq@}Cy9K&Ge!?a?kW&3jkHE41bHl-fE8%AHS(zMi2>vq*|D|J zu`-WIW&1tV*sTr71)Jhu0~_vG(FrA+4zg3JRhB#TB*myr6F`vA%m$JmJ6(we7qz=G z*V_L>+f`tj1CN5Q1r*YuybF}5L@0GarIQG4LVz>C(5 zQfrebmHIz_Amb!K#bV2QNCM|m;f zXZBxeqFjK^Q+kzDk{$G0gbpX%TMirShUZW2%UxiP2tH%X1te5`)ie6kKndy6+*AOyP;DYZLN`4O(bYzBXujMA| zlPA5|U%^hI)XT^wgL+Y}EN>v2@F#G?U8|vL-s)goaBST6WrpIl374V*Yf9JIuE%XS z%1=PK%PlIJY5zWPXzzAWa!${fNzr!{=g zdzNa{T`Nt(fi2M}yf8sF@T0wtrK+LmKtz%0JUT*D_%B!&7MBO9)YQlrUO(vEqz0?~ zi!3Jh@iJD(mVak9dOm5GFZX?qm|pgH7cbObe9NBVg2j-4kWdQtc`UDeA0e^2!JqCL z&rWgIwz11WDpf~@a2xaE0}qFa^%+qQ6S(fv?&5Bo>^BG+;+>W{Ooz0vwW;M?;!z?t z8Qj%wFAZ_+%3~pH3{C%DL_A(*r?X#G4 z-SRf@R@+GtzXe#1@+uQQ%wP3p87;!W2=EWq18Va=S>4Jy%xn2*E*bEzdqbMiKp9>L zbQW+kuXkoHf{2qb(b(FGqw&=k9RYmTB4r6(*-e=AC8K78wo>A4NVea*3Ob;b9jpTC zfmnA{`d^$EX)mWIus7we{@o6^|I=jF|Bp0js$h5AOqY@a`cUHlDL9Rxhk%ZmITVm} zRer8uHZUXIzxAb}Su{h7ii$G*!0oWz2gW^c-9Am=HV;mbfNmxz-2uCEMH4~W1Mmwv ztR&KGV$jW20~S{iAa!JVz>(0@s`G*M1-Oacy=(kwY#eac0aq7fe?n*-x0SWx8+0|Sa*#FvnW7{ zL!>dV@sW&zPeTV_Kc{hzH@H5#i>-Bm?};)_fnFw%>#n~;I5=P|(100u4wVAbpVJ{c z6Q4i^AGO@t-@i{wd+8lG=MXUk`r|UyOV~Fm4k+O&4afLlTcDWjt_d2ZpAn3%W6OA^O#Q2qTk=`Wt!yahUQkfDaE zA27-`43h2ft}zOV&N`rb;3u$tsTHMLP1hoYDU5h7sExRzMY*}uQ1PLPOH2RG&Uynq z?djAe?T1N1+K7^8|vN$FpZN!gU;LKJ~S=xfI_X$3I*PGrEFQ~ z#6wKAP^#&I>DqYT^8ZWpFbN~g5wRxi)symIMxB|&XBh#GN=gBGbP&ht=g*%(M1Tp4 zOAi6+DA%C(`n>)YtU% z8&p*IB2XUmnqz=>H3No9ZgmG?CV}UG1|AOc5l#UCfoIR3e}+!OrR7j?ZbIREgOV~i zT|h{P5+qqLocDYL4rpe5JvqG%tiyFP5U898&aJhlR0PUxr(l7=UIgYRfoDUZU}vXO zI1>DBK%E2=F9X8CU4hP0bXc><#)6gMxbmwgSU0zzKp(i!rTyIe)5i7z*r}$o1$Y=> z9H?n%@NH^p;#9Z_iY+je296Tb^7vIDbhNY(EyL%@PN357>2vx9(F5vdpvTFCPF6W! ze~KY{8|W~Q(m@}}I8xCEGcFiG|K^hNdz1K&=gxhPQX4zHWDy6z~9}37gch!&_F1(1D#!l z3`jHhcoa}}(0PRo)IuA?!p(yN!>MXdaA3s%8`+g1M+M*BztQ0hYGnh{%M@$^}(^`_fkU1U6|kj>ZX-jAG=SQ0(q{4RfQzy zcmAwqFWpXWAqBaR4d?j2&eI@PM-fY*Ggsi^ONl6JR-MogE%aqgo-{q?{@PRKgGAKN z$x{wB_X$~W-<1do9D?I*;+s0bJC}ovowZt#{WbjBmX;5yT;F?J4=}*e#A{FE4Nlz> z=e^te`}Q6dE8Rq7D}(3loxqWbL+;(&SKG6GYu#aY0`^jEef_;x=9igNLVx_gX(jH^ zHLm8FZHx0d3PMLaToXP4I8RaMhdbN&+7qb@w=Oy--29$epP8aOVt(&3KO<0Mx_vepN%yxXfGu4?IbmClARs06(GQx}=`L5f;=XxZt#AV! z)pqvw>gh;9P1diK+}**FE7x3tb?Ib@Th9pz2%?DQ{xVvKi(@i9Y|}OZdH>L0Hm0lF zDb6-DNg!lDCI4|q*mpG4Z5}g}L(wB%w{`U6-9_I#;v`HX|Nrm;Fi)Rc)zvmM#X*q> z&oh2WZ~62xGdwElGKYe~B1p}J@2^F-rF^A~@%R5${(xBfljX+xq0@SH@z#O4WB-B3 zk6SX+iVM0Z5j$z7^)Z}={WmUL1G)J*B9bQ9zw>OhG`nRhPSZ?Y_j}+Um$Ter((p}k zIY)+PK z^x&f;xc6wN*$7HXN?tI$Uhc&9L03^xdG<_N4BEAlt!->=x7rUka!e;5&(6-?p`*k4 ztR1Fhd9mG7<#8fswO)H~tb7mu6$8<_pSRAdDX~k!jwlL>t0RRLKIZ)UcWBtLyV-IN z*A5DRAjTw+XJK_bG#WczV1{qJe5tUt>U0T;w>K3gh=ZtN0vi*e>nVa)Yr_&mh3+yp!Q-u7ol9|;_`v2rm zW5}0$AVg_hd`=s3nPP=zLiz5FTNPy*}SHK4)i#&C8JK4o!K6 zOYT`OuR57c7T{Z3Z?ST-iR-e+;gYdmZx9!MI^h9#Rz%-cqIf?N4Fz<8+`%{E zzy)1W{#=qD_hS$r1}ueFj=0EwG>xp#4@qnDf9D^nI2dW-A@6_uU_|6TOBVnXaK;3V zl9JMcs1_P?QxGbVp7w|aC~+CG^PTmpySlCeobD`I160%1-~VtAvFH5}|Dg@1F%X7P z35#1Cy4Oia`~iQ!fe(bxBi5StCQ8x=4%SEm$Qjv#YFzzju zgmDU@H7gHKBV_Jej|=y<1Ri|Q=o&!6V|}~)iiCvZ4l8TU&9E!5YL{rL==zgszDL$bECa+6%u>S+Z9EC zD)XeeV8I$Hb6^3Ddunm<3hD|D4q}e$Q=&;sO3H->oW;dWZ5)`Yi+uM&aG*T_Km^i{ z5ULt_`$7Qu1CXVb(*zC=F`xlR^#xRcw1b1fe3y&sb($vJ!uBb-_IbGPl>s?yj%@9Jt8{V7XS*(G6wkKT6#AY zQal=9jJmTmg+X1`hI(faK9=kb0-00v-2A*wt@qUz3{jv@_Zh8>m2f~=h8HL83*OFD zum-?66Vl1t z&;ZL6;dT$1h!$xQL|wwYc5Nq#_i*H=N?v$ELJ%xH8u%JA7=ZAgK^_tB!K7CxGI|V3 zHXEFna0=BAoW9ZkZF2G@Hu{LdkE}xI7(?u;dozK^9K?w|XyV%E0WK{qK2NmyHM~Y^ zCgVxj*_f3(<<9J;MxnvImc8P$i# z8)fP|GUjMBq}!&sOZJoLVUAWuu)vzb#%0~Za8J}bkM)Vh<94+z0UGRE^9F}sLhz6G zm+=C`Qhi#W1HA?R*Beq^=f9lu%*w$nQHk5;axziW4399GnG3EHlKCy=&412S!uWY? zj#hb_o5W?GqBfw{P^!WLa>2H^skVrc1eVmTw#UCzxwg%#T!b$bn7UuW$h?_ry=Bz@ zoAh{p9S`hZoZH)Ckqy_Zg?aP)R*+p`nQx!)>5bZ?L*ZnJ1J%04TNH}Y;_cMd^2Xqe z$VmCd0{ei0XE1`8`u6D4NiH=`=+&l;a!EBi{?F`|yDIxuVC%z09(it>Lk1^E2}i}K za_!`=4ST>XQq}%6j?~t+NRK*((QS!jC&gT!!10y(72h>DIH+7{)9wrn2KZGSNuGiB zg{nW`+=Fid^pVq7J9GTpZfq7io`}ghwIEaf=dGR0$ z0R>NVXaExIv=KWVF0$c~3;D{|`Q72CQyb^uyZ2gc4#9S0s%7k~uDiT?1_qhJ_#}_8 zQ8}A2q|aoZKmHYe;WE4Vcg{4tm579Y3oC?;fnTY1LyGvLPCxRCVPe-WO^YgC3G)}{ zGW7fK10LkrEVgY{RI?uH-Dr^|v11Y_(HQzi-A|CAp!mNt$Ns0ghE-gA=L#nl}On2u0U>5(HUKb&PU|X8)!9;9^!^kIT!9yB>|v^sLyy*Lbwom zpcxydp`pR?c$XgWZbS2s?%s0$=Js}vVgQV7>ZbzM9BA_-7jh+meq-cxne^@f=uzKY z8zqJVE2^Pn%YTg=NcF(TyhG&0i|Yb{f;*ScP^@fhHQ@N-d|wSVC`5CIbcYGK56K58 z6r4fR2uI~~L(ekwpr(QlUu-iM*x(4R`x8wUpmupUoCJaj4~g)9%Pq^Ur;7FELZfV#uQMTFWs z+|nf>CC&6gp21j4t?k}W(5E%Uy>fh3chfx0I~N? zP0vE+plw)QST;61{2h3G=2ZqEv!5M+&k8xM3D^%^AUbpM@tlZqb&^7?g)q&90jfsFRki>j`{d$PH z!@#gyF@q#5hA8Z+^Wz`K-WQY4Jzy0BA=8v+p{g2xY^k820G;S}I$vKck8gwM_$t|W z&Gsp_dF2|=r!E^L;JaaEXYV?{PBYz-iTiM%-!}jREu(Px4n+BrdTX~Nv3G`5iR=J<$=r!L6Cg-~WxV8}tFuAa8 zkUxRdGlE0IfWMk}R1Pn6> z^|yI2>7fheGjP_)hrwu4>FMcBf!D7QfYIjek^;0(+yINoAD|zlmf%<|0%Jc#j|AkH z0p&_pzMI&WVV=FRtIlsDToKfi5EQ%+FON&r1s9x)flDbs0>`448t9 z4>m5J2Jfdxx_d@bUYV>RGELC^ZGQEI4H1xW0qWWv1+ZCBR$#h8&sG4QT4YoSN$Sr| z{wultKfd$H|I*{?Ri~g$m5om8zq@?qF`{B(yEVJ{1=F6Ip-)_&q&BcmMz?R7PYt5p zVd2;1*t=a%7IMKeHDz=i$iz4u^>|5BaJqM3&iiPnu6e7@!*+Tr(n`J5nSwF8&12;l zN?Wq`=YJ+J{Mc49s9sFg$&9DJ%-n2jxT9X_j1i}z@@;1MeYOPd)5~P0C^)SIJNlPu z&Uo(YJ+kbA)a2O6|ja!L%W;pBxd} zt&9;wHh6etbSBTlXvWmmRMH5nln}c-9CcWG+PR)W#DEJc;EARj{%c15g;AjKJfABV zezI>5`;>OKx%Ye_tD@KmoEaC~*8#l@mA}O@=+yM~u{A_T%`>ZG1H-|T$WN%EGOS!| zclrJKm?z6I)=eEbKsMVwE#~jX)Y0H!tZeK=E|Zb%2`eZNqN;sbm9+JZ1B3+2YC8(i zZ#y~tjpP;r2-K(6C}6jH$=6K~ZyX&HDtOvNA&1A~`*Yt!$oZYb&yJRMu0R7E))Fb{ zp~W(;x3{nMe)-ebDo2${-ANq0nxO#UPcGHOB={z2$y_3Xh%2BLC-6>Y5xAy#(IKQkV6 z@;@3RZm;xZsbG-3FP!6__{um4z0@hO=XJDw-&HV_vM+o}s+b?oNk+ZM6W?BeEicD4ei&S9>gl;o zMvQ+FXumPWLql^TWl&h^lsex=AD|N!_kbbg~2R zY0d#Q4h|pb=;#ecz=BWJCKV5d3bE31&qb9?svoZsd5&+l^1`JMB}dH?agBzbwg z9?!?T->$dY!?7zXK)CB7>#NT%N5!6=+O=n?$cfxXUS2=nc=J^E^5Cz|Np(Ki{Zk|B z58~q;MWmz-1s%V?I3fMrnSA!MwK7Imav?J%-l@fJe&GZ2H`gY)#r^k_KqvsA~ZO(y6Gbs z9MtOl3Yhf@*(h^zL@rIdzn0U-7##doEjU!Z{^7!-xnq-O#l^*ye%tnIwl-FUCOL=S z)FH;(r5`XeuM{sarOC_>EiDbC?-0ON|Zek@o#Xi^AHOB%cpWEgIN~Gc%hfw4Hw; zL6q?1*G6_~`BmyXcW8DGR`3qyXg^^1!kbO5cd`Z*F(5jFLuMS>_w!Xg-L#kUp6BEa zT32hJPxKvR+9N3FB^xmt(3%lKoa26nDMoLYZ)f1MnyMEFR`j92a_F>#LoZ_x<$Oqh zJ`IW3_V`dH(0;&CcHED%{F=-~9Q8`Xj_0^<8_8(sH$NEc8y2<>$#D z0fNV2+qUNgFI1v0J~?ouh3rDlb1rlWs_tSI**|>#Vdr??KUg+RD99fu3rTX8Wm*{C zd?6>dy*-~-W~C%dO|W`O_RnlbYo*0TOOp3wx5q5i+5Db+cGun%S8&QYO5;RQaIu3O z-W0m8N4~n|pWBjhl=jSk3eFsLzs~p#POn*Ta&bOA|9dxzvSL@SE@4%B`YTI()Hwe5 zS%0&?9mbrfXFXkwb>|%H(#9q%W@S)3DZ6Za}XDCA+Dy`v2&?bMmof8TjvO^xv4qMmCzUG0miOgV$F!n+bmuz9sVh`0d+|Y0g}{IQQ@9E|}G8 zvNJ8Uy7!ZslO*e3pD=p&!{OSRp(e?D6F@A7s|g;>Q}VW=`RSZnWZbZ2!*Qr5_1SqP z!I-MvPo>hKMAf9rW;`AT6L#CWE9yzdR~?f>;;AEP)532=gqgHtQ=t9+#x%2q-#F%9 zkgQy=IpC>Ku`s!dR!o}e{e(^AK0byI9${VNkdcwWTdynMhsHdp&4^O>50zy7?Pu=O zzdA*Ih{f9Qe$}@i^;ZXV1uX|9(@ur(gpeIMLW64d_wS8IzTU=LRmK^elJeBpTPWNqW^|6Rq}bb~uV!uNz4M=m$AX?<^G!#P=ZDWU^GT@Rzcn8zf~|kmmp0P!vR7MY zdw`nYzUdSAps_}Q0!p!uwuY!gy#L8#bH3Tq%7!Go^`+Io{o}iZ=U>-xQDV6sW;tzP zeSQ;prtc*>ZS?$$HlvL(j~`N=b85CKKc&07M#<4wMnB8M(CEP&4cf0=mMYNS#)#{x z+f@roTVoMx_Fc5B%@j3brD{a8cYl1WjC#+QJc^b%``;($r-$J?&T*&bk>8gXO!Jvt z{P01M|LRyZUm6(?-~SPQLi3{|uR0C+z86;?JmOL{&sXEu!q5~dNj>HBAf9uFVJV=l|$d_d`2*4ztkbS(wc z@6&S&Z#~Hlwp3_0j&%2JcWcJPu1^K3glsbMqNJru{m%&7#=T8&zx>NjB^c;R$@IgXH#SYyV=$=J-$XPlKH}0lee6+pUbfw>(HJAF<@Si5hw}1w8qI~jd$%*i z>fn6)7AEvTBqpP9k%EFGk+XKWxh*=!8ELFE>f;F_8S|P83){s;HY)gi&nqPd-=g>X z`yjk25zMv){x#t#Cqmqa^j4T!s{ta+0u^z3BS%s?dzKw1gIaOjTjOJ6N@kz)Y5xuJ z*?nY!Lgs+Oh(aM;;#??U2I0U0^tipm#hf^8I5;?H>i8XLUcsqCCEu)^Fi8`saE1_j z_45-L5|L~PJ|hxZb25Z)l0S;~mf+SQ^){dqPa<6IoCmBZ;Lr|)q1y^al}2oKx~?F6 z3}&*;I9sXWSVj~i(V4-iP-vxr8PY;Bl5v6V%!UHQR6ks1Ba#tTq zUsCC7zHcl#!Ng61 zV}BFsE;vkz==9*^iahQh9E4B2t#ZEXejjgPc` zk&%`C_U`T;uE`wpNI2qs+HMLV^{|R!0<; zU@?I!ix@eci$Ax4R@j_&DgvnoNcd#|&a{8F80M8A6tApsD8|-9+1QwisP7X&i%O6! z=5V}*%&;9+T)c7v%x@O9wv?*1ah0sxe0;awzyIjxygydQn2tP9fPis`wB_~;y&V80 zQt4(keG#IJhTv^$gt2WR`W%!GMf-DzOh!*oLIt5c&s5gt{CP|G zct|8T$dG8kg=PqWBtum6P|2yPucriwqm1#4>^1+Sq$K7e^g9)Z1%Sb5yPP0b=VRU* zWtPf};um|R5vfTFqZy#JkcbFsxD**#St-zXFg&H89o73khk4GAF(Iit03``>67q5{ z!9S0*MoNS??0P+gYJ@c!)fKYQ@#D1Xu-`HF@nJ1F@vp%DLd8YLiG^xo^*0V^?zDJ7 zXIk%UCP8;2#wzL$$WnZZgvaFU>~oH8o40In@-BYbko%Z4SE>WkV*M@Nn+WQtYHekp#=5CSJ}TAdwXwI^k<1R5M77dVNHs zX86sUwS@5#W9q5j#yWxOh?-YK5)u}^M8;MFsx`mL1)oD!Bw!s3>}rHr%=WlcZU?~! zuTa&btf-flSE<`1J7BdSJh^W$3c?3b1Kw`7WqE39YHL|OHeYq>!j^*z-<9B~D_J}s zArXv-9!#-Y<)+aMUaAd)}o;w1n4d@=Y} zqYzKJ8GcO^8U3-Q!4N##@4T0lM$UVQ5!@bEGe*+ZGB5VOML@JN_&3#LzuK~Qm;yZ1 z(>qDA)WH}=6eI*Blo#ooJV}pii=jc?=U?xkx(g+dFvdQIDNb$*$Rx?%q2pOH=38mk zLDJz1Uq~!0EO1|0AjI)+h2+l@z$CEKg>1>y3p>` zsg60<6&aQQwL}IK^36NOr>5x2Mrf)Zv5?cD<;v%nBKHaTexUoNM( z1qq2w#3L6mE8~Rm>+ibp*GE`Fl2TGq*0*(ZnfV<)bV#_WpCyJ9zEK*e@`R3AWU3YK zzP9Xk{GkLjdW5vw#J%EXCc0et6KxP&Az$0CmNq!yts0iPZU?>^fD0)|`4v8P=g&Ko zZLDArO;oyFy{e1AQ8ZFA*1`!txC)>lg&~v;g=SY&R~I;8NF;KyiG*25nws*Tdn(E+ z*F#=NjZ%_y6?YB(_;L38Hx;#R++)CCV9-^vE`mB|#}MXEM7!KGJ^_=yI&5>#F#3yF)gC3P9)(hji)ENj(^oFtFLDMHN0f@fp*dCxzEN)#-d;HO1ItD&D zB^#Ta5MCm&5-o6Vy#Mea1|{=HQZAU(IId0b$$5$s&Us9=-|!bi_znobC=86OQF>O~ zp+Ix7w=@6}1a1mo7K+tc`&&7)chA#1EZerlk4llSnnTWx8D+OThW!;Yu9FA=O^_YzNAD2{J!% zIRT~$-3>}L2fnz2I!j7A36)E2XZp27OJBxvy)dNTG4wRa8 zp+PZ2eFy>GK!w41y5y+3IyIF0z(>iLd!-Vwu%xNNuw7Rub2d?bAHmFLL zc#1Dfw$mL1bdX#tYFQYw_g-lTc z)-<&s2Wzs5uC8wKEo}#(_pc61N*1+k0KMsF<>1&unEPw6Mrhl33qJL@#!HD6Nfc|p zMv60B%r#i5fgrzAB@EYmc4g@$9J< z1=NKCYyc4(G?P2&v!@)pL!vifHf%HO$L_`wV{pc~)kO~P))HMd&_t92V?_YBq8wjK z8%L5nBL%F$bSn6F1I9C84uWy+Ai!LevuE#%UtheC3=5i?h{#w>;2{uK1~Ii^f7tGBq_*=Q;wA#M(rPg5{S9Vl zkNB<2$y^;vYIlJ~V_1A)8@&72x>G_`MbLwJ3X$}V9w51!BRw_M0@D*G7;SK}r@wod zZ*08liWaN1b#RCPANQwtqYI6tTkhosJyu&@Ckqpu^rd077$-K0O?YHPVg*78sBpD_ zH{zp!E$#Yc?FuJVox4esZ0_1&Wnbi!(?FyIEYEoP6%W>7RqTq2Oxc+tkbO!IK@gN2GRP37-RWKa7w7tYXl-uI=VWcQyL<5}y+1H47`NKzuK5%$F)~ zw&hP@S+4y5>pW59z2~}L2K=P>1on|GR#s%PvTr;T9L*lnTsLWYbMVS}QvUkU189De zH|}(hcC8o%1ap^uzHB8HHVgt{mw-9ov%P^(>=FXVERi&Y?eZ}C7bfsksq@9blnb z_cV?!86p{HbGeAU2cVd55V!67UKeRx$8qOrO3DF?D$+vEA2e@;hsl*ISBM3Z$Sx;@ zQ(wvJdSLl$AgRNAuP26iK~-O$74y+R&$>BQHXu_ShU|QNc@<5T78ayeCV3Z|8yl5y zRYZVeu5L0Jb~lpsKz)=j)_Y$5B(A3z`G#i(X?L=8d89f|{BM~?gjrQmy>k433i+$GZ<`F2L zO2>~!E}zK4boAh`^TXwZAJ|PBzObQQ|2xVrcYTo-OFb@Nl7eBtENM~7ZMt_Ga?EOR z`c2XoB2YR!il@Mus+Va4GhJO zjtKCBW{D$ObypV$W;1fLzUu=8)?v%Ra!9l$Y7#g|5N9Pk9e*SWoRRNe=^g+j5r%g| zWRDODBE;?*XKCW95t2U|;wc6_q1EEhK#2edL=q@x>XZFD!Di~y;Qkcv{QJaC7j&Bn zkOKsc+yo3qZ~-_E&&b>IdzqA#m5KW-UnPnq*k09ODK9{*%0g;2!x7PuCu&PokL3Sp zq9)G=wi3l-27?Dzob`OEqW=k@|BH0fe^)^Nf8$aAJ1;Ou2f>3=e*OB@@vr=1Pp$jE z8i*Pssdg!N&MzG+xNwe<;(bC1RpP=r&z_TswHZ~%q&>fT7DSq!jj-g8AW)Q!dJVhx zM*dDx7b}W<)_*4IHgu-xTI1dwvP9pHT+3ek6;7jKW*P*^k$;Z*G|OGNsn#8WAdo3q zy{1F5rcC)1sTWa4ln$wj3xVBw2^!V zSXC>>7)YhwQo7II{#3^SzU z>>g{YZ_mL`LGZzWBRE&jKsAmgVAH6-{J`3v1I zc;OeYxL^}`(R3BrRt<8JeNbU-Th`cU(wI$GXy1k>lV2K~y;Qi7Dv7Wf*6c246)z3S zj~#~?muH!ZFB*o({s}B_S7j93kz!$Q@BiaO`GE_KN-*&oq_ogw$BlgL&-I(O`kSSA z`@ua5gSc+{pCb|EVbPX-GuoHdhuL!ao^N}!VVF6s=+v*G|Gc1@@KTai=y~ggQuZ(= z-uA!Y?hf~P(lW)ycDE>+$Ep1dJg;~r4_~bAoRo@r@`P|ISv30lW}NxG!^ZY}^3eh1 z2L~N^7I~y2zkg@tKd;d{32R^IeZlL=W^arB-?Bbc;MDB(K4G1FkzeMD^ex?%xK`hfAMlU5qd zR(7$Y@G2A>vgf6op9<+)p;s|5C{fz*X)!MgoO-ip^|93Dxm}M2Engc)husI&tH&?I zOe|ZgGODQ50*btP!K{Rt|4epZqU{IOTPF1KeR1&%U)~*hAnAN?-S8DfOYGz44wJXY zI+21lbM{A#lIEDIntFr9Y?UuWP5+Qr+m;u`Bd&0WDktkhbfT>dqklW&BsEwSV|`oc z&P>VFDvzQYyR*-^A?vR0wvG<6v~;lC1hgA!>x1}*__Q3wY!{Gi zCvtUScKmw7wb|M2HVzJBUvsz4FDwihzhQsqB_-s_+i)^LV@0Psss3muj>tjvblg>a zx%4L!-{_9LMqwEMXM2f()R(Vk{b86`-&B4(A?DWLZCy9TN_S@qto$TbkL;lE#tgEq z695g;V|yrO5VB2~-{-cVw zEp!{?WUTj8Dba+e%sDDrF)?$6(wnZ>&zI!tmi}d=U)<=qm6s<<>C?}FCtKT6zS5rL zD_^+a96#!);ft(Ui;K3DJ5*H{UO;SjolJbQIctZ^ZtPt)Td(Amb2abZFG6E#a9NAy z`{j`3MH&*uMs zPi5ko6G1RUW^dmTO?9L2M93+|@99cz^WdiS(aL~y?W2l?7y4PH zF0My8b-7r5e?a|U)mfm7`&#)Go}tc-aujO*^i7tvn+nBR0lXdi*e*6u5t2LU%&L{P zJX6B!ggN!$L61iW#o_}(60W1EFV@`BqU@M;eC=98f)8lZ>eM-FVs=eUOT&$i5a*YF zzN&u_p0QWDyQk9aNzLV}&s$&Se!r*RI1<9Zmrot&k3Sn484q?8=N^~}iH1V`FRecS2FzuG)&Eq-MkF zJ|*uD<~6AxZjXsmSIxx2BY%T|`b`1{CpN=2^eb}Bq35&om32Wr1ut3%lP@_$xBqOf zoLz9xOX^0mTUmXJNr>5`%-H|6q)2D$&RyviE7LxWG0jZe9MSxC@2k<-XfEkXsH>G% z4GyJTLJz=>Uh~+Ax|gF&uFp%ZyFB54^Enjhe8n@}H{a)o^p^7na{Kk2Dv_1>vSa^# zQ(l{kYgag8^u#_*txqXdzuFq-`unpYUZa$s`G#Hq3)AYLa`NOI&7JmLpE-IwX4I`t z9lU(`@^j1Ihta4twdEIMq7fuielGsa2@+{Qhw~;|c9-Q~oVlt{XIR)d)8kx!=rEk_ zjARcER6JRZdBLQ*#cHYR+fdJoBNMOQBv*uS*ZWDat*y+f*kW1o_FPd+Ipkupww-B$kEqne?^uJ-*WR)V|kc)IOgzG#=!1zr9i zg_)b@EA!PaYY7Q;f3K5b4u?0a7y;i2_Yhl>!Fp4#GZ`U3 zoBoFZCUh0G^RzB3gqJAOil7jfkNnf9W32y2U*v`bT(90f)>g#eYx+W^2Z;(y5qCb< zRu$Z}Yb(o?B33vUy#6tCJ7u76VEEy32N?oRNAT(6H=af*KJ!a61$G;j?;pP*?a+vB z;Ha&r2Qu|(YD+XvL())6dzzgeEtdipzT85haVGuzPab7`9Y%6Y)6Z29-y^wu`~ zlkbfq1eXia!lJ4zzpdyG$y>^e5Dci6QKnU&B7^2zy}N;TRLRZJaRiNZ4)U_Rb^=6)K<_J;6#GcTLqun_lsN@ zScL@Y_$@5acDs~;e?kHuX>hep*ihU~M!{h!B_svULjMH{BrwYb9ah*+B30~v^lf0z z5HTZZB<_wX(}kCeG&pp2i-W_s=CRoAI}UASrY7_xoYz6K1@J!FAT}C9LuOmHCyUiq zfk+#&FlT~0AeEl#k1Agh1_yL@bV&b-@;9~h-882%6%-i8B2vGz_A*>c%1g$Spnv&t zaB{NpK0aQYDn2Hs=hB7AFLUT5%7^JR+mcAa!rGs%c+}!4i(8FxO!y)dXQDFgln;pz zq_uOo?0@ues#bE5m&ov^B**RPUM>vI%%sN194d)*5{MIa6_ciLQ*;j~Dq=$`wWr2# z1gi2qbsD{eOo32Hk_?I;nogS^zrX`O9R*4Hi-AbU4%}yu$_r~)!^tPg7xNI(-;foz zFIFp9ew{;=axti4y_uy_?aszBsEwt=vrhBVU)j1gmbvX(Osin4Eb#0X(Jy=%;v7Qb zIX_cb%D+mQQgq(xq^*tHZs=hTY;1X_k;~N?BXs>*kHz1o=DN9#v2s0NA4nv5(_CIy zC!R8=$G$Xicc&LMDGMZK--@tDUawB2)s#k@c3oIDxBpr;F53YAV&nTJHEcLCFjXK< zqh@SwV5q5xd#w8_KSsQ(;?kx-Rer102YLYR)V`xKyU=P{+LcR==ZDF!s#vD_N}r(~%}3~kKd zX#Bv&$g~Z0Al5IHuTy7+&T^!;m?>>`a(eLaApwzhW}mIDu5FMY(+pcow3uA%3lEPB zJe66KoU&(jq4b#ddnEU(sUp|$))Sea&$-4KE}P57JBll)9!qbAwPc2;Hs6%{?b~|G zkN3tkwROUxiX;^#KW#d}b!`&~oELE#85&q;S&-p>^QnHZZw17cL3s`cNUNlcCz4N)k!h@JLO64PWd9q2y;i`+*la#~IuFo2WNp9>)ZmcDWDNWs_!~ z9c{PtWaXtKx#kTw%7!R9 zmha~>*f>?&*3ntJXL_nF)wV;fWK-6cv+8U;S+iOFj0QEL-%J{fcve?PmqScE&$wh1 zb>CDGwydhF+YB8~&#wY^Zy|@4zzh}+qB!P!57&AL#@(tp`wR3Z=S93heNpjotcgfLuhCyau1#!ZrTph zuHEH(M@o6Ts9(Q+Z9g(``uiL<2YHjS$-4Un&y_?$$>mw0vYSp+KIJ7ys_OnknLAmm z5p_eXvP#BCHaRw%p(W&TUBcv>17AGsV?mEG6%<&_ce(m&>xEaW!T3f^$w?=4 zfWoWJA#EHCj4_-6;cuy=d!#wEP1DkjSK6W7h(Z{CsXam%VdJMMG(&kq)U1pOP725Y zNzhD)H4*9$jDY8c-B_6P-Zh@G$qe$32%s)=^5SSN)r*KB`D^e?o@!yWMTEi`pNh%bsx5`pS-k%qNkU zt{qcT*5}!{KGVI?dDdMf&nK~1K(C*nl~%6f4=u?*BqZdW^(RM3M<=J4`1s6DQ7SSn zZtgb)GJ=8=qJaWQ6`6dmx>~ot7vuTN{+Z-|bbpkzpFBsFEgOCr;}7osT+{+2YQAYV ze%OD4)Ba-jrVPW4yUlIRR~GiPdUJ}08s~oYaGmGP-9-| zsi-u)a3y>;nu0>0WR{2P5x1q2L(dMpWWtrJs%t4$`U)Ar(M4;Hmqc z{dM1Q@7_R2SLL$#1hCo(&GXK?)_uIhj6=?fJbpnn9n=@aWngyZR^iKy&{y$C*vsdO z&uWfk<>eIlRoYy<;p-7ktTB5^=`PoPYVROzT5A0&vG-;RLU_(AGv2QgJByh7<9w^XT(9F=~_ z^aldcLUvize^6@^U#GUlMCTWrWLeEQJrT5ngKclVP{R|U+Dg5)j`N(ud&($R_C)Vk zH2AlY`F*qh7GAPj>}~m$KdvkJr+U{>gxhfAwmo>rQ~G*UM;%LoYUn~wGnwDNCDz72 zJ4M)p*Ocj?w~Be(#}rTU?Es129w4$0j`0xq*8j0D9={Tx+D%6Q6}Qmhg5TWE=lVP> z`r7;H*uUDg*I5@r-=-vqciBq}Q`8^-)ZKjp0jYPTi2X_Zc$2Y{<3_Ongh-3Lv)OeN zR>uj(QTfW1m&dq4=lpv;$+XJjyF0w%p+2Ty`^yzY>1;_k;q z+2WF3|4=rEvx!og&OJCknnR;}Y>VuPMeXHr>=L2x~WN+ape~)CD}omT9Vs@%K9!UGJ!<#ynOBLf;;9$?;o$ePML99 zy>1tNc4G2icq7G*(Cb6>AMR9$1}#&EZko_{`#^5SB`{(4_zC6QI^RLA|D#V4ePZF^ z>Z=eS&w*YYlZ4?(t zFeB#&|$ugeaz^Rk*>`-|?x>NFF-ur0X>Fr=r?xzkmM*ewXyy=X1js=d`r6 z?YnntI9XT8Vp?B}zH9T!<%6m^M?Q_|+Yu6C^sivtJUhQoD`YN?c`2zcYX>RO9J{hQ# zFXh*zKtB*j2#5s7s8?PKLMQ+MZizhk#~HdLxZdECE6CLv7?K9m1*wgkB*8@oB|Zdk z1D4R}z7#pdebGs2A4DV?)Md(p2M^9urUeF);bJ9zJjYsq76j|l)TBj7!;lBDgKKzh zS^GL|`ql(eg)^1Tj;G0E{`{qA6)Hv5B-o+yp;No>x{Xh&f9m5@%rt;cH3@?X_j_n)Yv25$;P`28 z%B_0~fIXBwJxk9r+Ou$TZv{6A!kLT&YgPbaIELYGw&MRU3>OzF?GO{wxzO6&3|akN z^%Oe?$2UBE^lny;cZc!W2%;RklP9hiVm|cvNYTs{N6Nu{K$++I8Y$mnRxJa zEy2h`!(8q9y^$&Rz{tqRUJ;SE#2{YO+kMxvGwTfUD5i_KWV6$=34Ri!h~?XGCeV|r z7`7$voirirL;QWl=8C8jfw*Q%XXn0@p50ztwB(VXqab!-prl%V9}>j%*yh>=;dc zv)xH%cfqI;KKi62(%-*-fd;Y?Trt>EDKis;;6IiAb`Rc?3au$SPy_;O9p8EvY7 z+srWx$QKqC;9w2_kBm=9{3v42zmJSm*Vfi%oGy8G;mrrkrQuy>*Xa_ye^FPgEuIUq zRzz0b6Q`L_(Q}1e#ifOvUA?`L&!39};6L{BZhSm7`qo?|7_ZZ}@7)VV7q9?9U|du( z&;a510bQD_IsJ;_B`-@Is`d=g)O#GJg#ZTfyu9Eri3YO?JKfProXb33wo$Nr?~c%PTK2 zH$hV-T%SJjq)r->6Z)I6M~e>Uui zkZDtoiv}_~6%g8KFTs_eFLZ#GQy2gWcGyRil{bMY%wG5kGDXkW_y(>{a4Z6_w_Q<@ zADg7%acj9(@dxk)dJygz*ADAhSX>On?R`Y(4k&5+{bP>-H0f^Qd?=APYwyauquAOBXin_|C4Fk-}9g<1t}5}uBk zVszU>d~RG?NSFg)g~qrcF2Vy5ieJPFKT8-g3*KEh39b{+X9MC4pI;u)!Yy|ySl@8o zAd(zFbqr71AQ00x<2f8oO0U}_ebK6j9vMCQcXw`v5V2~0gB zBLgUr5N^pJDBrT$IgD}OS;YMoz%AVrD*ZMp`qM>_%7DY$fP+scM&O(WW#Q=fI59Nf zza=7~Nd7qHTlAM*djMNb|FuzG!ktiD%#IE>;Qc$wb9v2S;&2+@X(BtMx0eU(vWy)G zOxPu`YJ6MCJuheC<{c)4V#p{L$tyF`DxCN>Jp9qxPAq&Kdsr`ZEL^xYk05)!7dj1a z^^HF!Vl)(iLW{P(0oBAK?MlKNO}H`uA0mS@8t5M@)=rrx-V*) zRpJ9=$ZO__cs)3;n7em_fap!E5z(OIf_DmQqXt-qEy|W{6=2pNdGcq#+%FE@k z^(w$4f$K!^Oy9H5);A&}Y2PQvpPRAb;^&VB+orFte|29lS1m4f0`>Jk?%L(BHKZL7 znJN=&KLI)QEx)DIJ8Gk{G8z>=|K;z9Z;qi%Wqz}OAj_OxTx8wRttiL+&u;nuCr$<# zr%wj)>YXv|={nCCL!Fi7G$iq*Aip`uH_*~wW@PeP$kSnB&mMl<^_wlV2JIhg<}(s4 zIFn9Bd9^c~A7LxK?H6H36`pYEOlJ}POkL6}B<_`Jj+4b&@Nq_pD9M55A<$LIl7E1ZlC2hClkMrIMa9%XV(A4qs@Ol=Q=+ZKiM|V TXV1=(@ZT}@lWKV?7J>f_<589z literal 37371 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 diff --git a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_13.png index d09a3559b89767eff3312dc4b5e1eb304547becc..b01129576f860b23a9bd6a86a5648cc8171936a6 100644 GIT binary patch literal 33486 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 literal 31743 zcmd43bySsa_a}T11(8xg5GfG_r5kA^L|TxLR=T?zMJW-H?iP`j?hfhh?nb&{_W3@~ z^R9QjYi8DOy)$dp_|JjExzByw*R|ubKl`)wdo3e|ee1z31OkCA{z^n1fk4?uAg=Y? zM1yzqQ9p3P3yO`rm=GeTi)0mnc!&@ed7muWg|$?QHCl*=!S3KSS}@`=x4c;7s6=Jao})KDl@?QI5ibA3VN% z;&|-q(tLErw|sZ|>>_@fOIcP9{|1HkZJ|(;Q@u$9@+V#{3|r(Y`m01tc;yznrg4+P zy9ed&iV(cwd=n&uS6k2P<+p`yCZS@rm!lxxXLyZ&!|o;3>KfMX-vxi#+NgMVh!JjX zZdh3R%ss;`jUzX8D24a;_v4K(oOj<5!51el{4OXEmk_6<*U-qikdpqwoNMS`ec4Y$ z7xB+6Bn0K~zrX2wB`)caY~4W7)tH&dwumqjU`4y-vQ+3$K6`%ZZfn?e(o-3K)&{3B!}@ zsQdf+zMCw!^Y-zSR<>_Ncyf5s*M8WR(v@AUfhZA!7Pk0~gU2F63<;x0PsNgi^sOWWYO z$nk{;YL!}2FfcHbmX!sEhab}YqO+5elcQ5B{?gIW@!MP?!*QtPsGYIqZwt8x?;wjJ z&ArX>l9rK?w*^we)0`A-6*n*mW|o)HJ*%qtOonrsrl-l_pL7rTb4x+IjoI1RFC<*Q z2gbufcQ*<$OYOp)6Ag5A;R2udcrtNu@r}`f^ukP6st+GNysdWSQb}{APs`$6pI0k3 zC29#G_xPpvm6RuZFjHAaPcKd7$NF~6?L`$4k?R5%`*`2*mfYRlPc*3=EG>~cY`Zyj zuNC890v9Fig|gXTAXeIx9U%2-C^(BIG^m>+9RfG1Asl z$h5SzA8}etxl-A!o}N*OI2GG(s&;ns^YeewBtF{NshgM}N$gqQlog7Tv!`@!%BUEi zZVDvf`kt8h(0+TWGfSo5xzp}r6cm(!Ol8LDP1I}Gj#twYO05@fEiEmvSxm(`OgW)b zQ&VSe!ad*SYu`whi4&5;UmMQt9?92PohU09vhVHf&3c>vp}_|i@7}#nWo2CSYQ?$} zrB-Z4eIf-Kj*ic#rlu+{PuJN@hQ8dRRkFM|wfpkri{W@NeSf-a%hVK^XE?pu_vq+f zqsm%Z346=kCYo2f?Tp8#r`9LCdcS}F&MPTNcRAYn0{iKIkJh+2hDGmI&G{}SwQRg@ zlmESqsj8TtKcCVl{%ZUwmvW!WS~p(ht0Hfhv+ci|6mJz%!4(Y!Go zW4^*z*tbymWU|wZGV=aPr#EQME@7qSZ}4>JI%x^2Dc=2^^&xkEI~RS+YHvxJfIowdQ zD=*!3a7npEz=q&3TLuO~{qU&g78jd}Oom5Fttbx<4@*l+@z2iTjG43>Zw?rDjOolxVxouaF4^UlQ z-PqLBTV`VbW+YN%Jow!G{5fOw@uLtb>14b0p|@&k%j3lz9a5jMh=d?4fsHsiJNFeD zy5Ytv;yWxI%2H{F<+1_0L`xA1`XL#?kox1t z-1>TZna%RbaPIV*t4EMzii7x1Z+b$!Fm`+H5xcTtcnuZRD^_K@%%eZyN9e%5w(M)v z=eEb38ymlRdqfrI$`AJUf4);~#Q7xe&wsYTDw-z$4C0O)zNp;y@83h0I%8$6!cxY&< z?MlxJqv`6Z?@>`1PyLf!k7s=(QeFmzf(2xkKcb`z17D>TBm|i(^ zunoB|ND@*7rhTd6{t{{O;Lyh-^W_fP@qMLMI*Sk>`nXH{lb=E$QijD?!`BOqQ+YeG zlzDsvE7V$Wv%WfnM_X+4!%OSS`5EcB_`=pS>59X2G-sdm@Ao;bm3w;4u|Z+OEwTzq zS*I_YZj)x77aGOPu{%cgS^Jh-J~nZl~v3fJU%&judOYVt67C$ zVr5N%aJMm07IJZR)L-FX(Rm2JKcS@!@Ipm5KJboaGpdhfH(|qzVbCaVJXjxI9nMWq zV(jYZ_!35|{5?FJbx;c;DWA(>4>;W~#mpae>)(@-DCkuSXCP{)rluNVf2=4iHFP#B zE4N*(hfh3rJ1tbgYw2CL$v8y2jlt+w4+9zcZShnVo$R&Y+23 zKsiT!7L2~~bS+a?SJ#l#p+LVAmfg>rm{jVoRt)!oXv*un@7oqeYZ4BJSS50>9FAc* zTYrOvm6bg=@XMFCRnB&PZM>E<2#AgQYl8t`_StqC<+iiw3GR=cKJ|rX3oV@#@~s~o zAIBpi5_fcT3_dzpE>7#^LH8fv@xb5neWJy<+<&rXIMVYeFK^G4$$)`gje{BjQEo)` zOw6KW#_WussX7UGT}z6Ia#j|ZfNbJ(-27)ng57ag!?{`lovAlGQBcwHY?ft;&BlM+ zTY(2dX+0Wq2IKZG)BwY06A%*5|ClO?gQE-31*zJ+Dp1lj9RIm2F3;5~f`cO8~|CU_CMH)V1@BIBcQ|pBa@#JHUM#bFLmJF;6 zcA8c$s;q@wuPyXi$+J~1JR$rzKwX!Ym)FtJ@$TG_OZDuk8Zx)A&@%SY#DD*OIX_b= z@H4gzLkNdapD#oM#5-NxFKKB{Tn;zVmAki|KObRr=t7sR!cHStfk1*{J(UObx=V@`{Q?!7Sr=9DZBPH@hCSZ|d>OSOtyu_lNMioi0>J zi`Tf6l^qxE#tlI!9tdU*DUZ)oHMfnjTGO zA)!b5S1Xm*-m0h~-n~ogUQS?`S>C+th-D{qbK^TbTK`xQVUT1;xE!D=@ubIMqV$EO zCDS)n=3fuTi_LT(ejrB1#;)O?HYOySA9YWuwtOYUwAk0W?snX^WYjo?_NKga7x?$sf|hV}(W^RkNucKlawBu%F%D z?q2Cl0eh-#ZT&g2$;8H{TkDB}b)U)e_}K9)8DATOcw~C@^YcSS!zbK1kmC6cY^c1S zayvLUOiWDN#KEBgC&Iv^%6nJ$!PP=b4M1vp1S7h&^D^bjFHei?iSQ|U=;`QoZBMne z=hy_s$CZ~iF}NJIy?wN_4}HkzS5{sX{E&8xos(*&4?F9zaao3~p3BUwZrj@~=;jyJ z(jpfS5a`Ua-s)*glZ@C6EzxPgDYlr_T(S4KJfU76&ZVZHKv7du%gW9!1G}%LxQ~Yy zVlkepQ4wySvUO89j7E{nWx`>5>PG9)i<@QSmJ+{yE>j~9N3ypxP{4bFexOaf9tig zIndldLjzRi6%>SvbPY7L->xUtyu7?`i%p~S6IxqebxIxey?giCQGvrab*}CsdaQs4 zis!eG^iqc_vTrsm!-YhwenH)TlEGtM=L?DMAKYRrd1|-pyylgol(}hx$k$j1jAYLN58aSOq&1xi#@)MC?7AqffpAC*vHFzYm99d1wKdWMAH0f1S8 z6OJ}X_|{(#&*#DcF=|lCdVREj+PDZ}`;cJ~GZht@tZcXIwp}&A@uZ|A)ppp38-kfi z*=+4tmk-cRmQze^sNENFP!`92>jf-*tgp$6FY{r^-TA5LUuGpkE=`tr%lF>YL2`(8FHRZPemPmqK!yXq8S@ZGnK~2iU z#N@a^J$Amf`}c2cZLKF%Pj6U zg@`qh>HUWxrZWM?@y=MTf`NDf18w!=SK5O#icTiy#IBc%+ApgP1>6Oku^c#+@kNtl z;d70S|uv>4&j=E=ULsK*`V7HxceP@JlfOcFhJr$^OAX zna35sz~wO|tU0HY%H4FZq4%V%nJt1%GSL14ZB#&xvE9CY;0^aofhwTYR8cq z7#MV({Aq1{$ing+V#5xUE803b4d~>qiR^OB0w<(epKjgrS1*RG;IZGR?d}c&uwb=4 z#n%3b>%m#AEG=iYCMy62yfSgyx?KTo`1R}8=qaan?;hgd;HY+Q zJ3heLKM)tEuVh_XP{3vEzI1dtOgD=evRxbT`s`7ZW;OrP@o+uB;L06dBtx1%`EYrL ziS|;T@gGn(VMCty_U-oh`T6S%G7nZa!1-nNn-9FayhcVxQMiIWPeO$NNxr4F7QxBM z`9mfydKuUC)vKGdN?8xt*zk)~0}BdR@u;L)ZA|186henW=4h0%d^0m?=bHmljQW2> zZzAiJ1b(;sEV?(VoDa1Be18I5h>otV2y9jqzgzjxHy5br@u+1Mb{E^BL?|%or`p)u zEVG=w29~8>YKijZ%^Sc;Eve!mKb5i{z|CfY1TGWd%g^`wWjmtT0}?zgqX4MAuCO=D zwy!8Fdv9QnPDI+OIIw4PdG1twvP3Ks&npg9K19o82qIsIIc02e$RRp{7h73bx$bpx zl5pGlLII40jSX9#2muv~m_usazHYnv>?>f9kl^4t;6XazByX9H1MEJ5_=ExZfq?-B z6BBc7D7#&fu}TE+9y{N@hrqL(y!o*L16%+i^L=TOfbV8zW;{zuIH9!i*!bw^eLIcu zDEuutx({!`=*9UtlDb(s_;3R={`7I#cYgXUas1_HRu89ra)OChpi)=}k>!83z64Cz z)6ac42Y?HbbtJD$RImOFHUEA~`H|)GQ=q&i_6`sA>pt9w9)_KhEdn&<)4fTjH@?)Cv>REn2#?npM|kE;>90!B$JnXpQYfpZ=kH}T2egT5tl_$7fMPT{>g@)7qG9K z>59C(_^Fy--X|vFw+cF+evIVlZjwI44ZrjM<`Vz&_CMdW@~vR$9}Q;*YnP z=+@DE>pSCkA|O&3YO1NI^g=E68Hd#0$488)SnB5FArp;%0*C3Ml;Qn+s~v@`*Iopj zF--c6nF^VzH$2^|T)qVdCyi1cFBolPj?cc+s>+I{zf4K>(UbwR|19i-bLam3)?qCV zBncA5>%^kvx+AK>DSa~zc*V|+vORHWNu8-!BGdC79{vkmoM+E6I!+ByMn^{xBg!Qy z_Sw5B1@biv}iVB@( zWu&JkVs38k=bLmK;JVVKV>l4tPfQ+{t^fpNt*u!O)&_L|`W2c_s=nz4WE{z9K}N`^ znF^T$F24`VTjyzM>GS5Vq>{ioz@0i`xk7MAxebRbM)FXg$|K`-6a-jK#OKTgoCM-i z@ive&uBWT%SY*7B;Ohl??F6xLaib6m0XRIOpzw0PI8Aygk&$3Qk(^9^$7!42MqesP zsq+Ei?c2z`sYXsN_eb8gMbWo@{BW(%r|%6S+o>!3Bb9K$OjyrY@NQO{yU?U_z=8T( ze0W0O}@|# zM17amyUwwLi84H|q9Wj9%UO*_+E>=sL%v71B`UvUHQOD(eqJ8Y>Vo@o`K^ytZjkok z8o6Jk137Dfzblj3Z~f5O%a1;;1#9T_j9L^>5aVo)F^MZ}Ev^D7;5D;*efIunrkd(_> z#LCW2aj7FJWun4CRu0$+0MbZ8g@o7fHS$*|N<)!^3p~@aqCI-=b_%ocUY?~a~LWyj`R>YVkoXb^DdL2So^e1>E zl$7kN7jGo+W;LSdgW?J7|@OzVj3^#Bo3 zTM;p5ScKSDMfXG8*T69K^$$ug=0=2`B)Dbh>g&_^iHo<@Munv+hwu3nSFG_8@5k%YSI2bT4e8*PM=%Dg*V_}$@HWf3+qGBOiY zE`+U7g#N2#9cf*W)6<#`ub~7I0FY)KilBL%y#aQxCF@V zhh~*CWZ(V*1KAwSs$Ys)ke*k2l2C!dKHv7Zq69kuz8e9X`8gtj5F$(?;C2AreOW53 z;D=hNhbWJqJYjY}cih<6U}9m>1}FjKwKt)rBMRIO4Tt0v@Sze1vA0m5Zf=~2dQ*1a zJxcjypx#;suF86a-(N{(fAyCy^{LQtNN2+$(B#X$V>dUy#PV=rV$QdUt+33mz33Y> zZpQMsK}#!XJY<<@H2-;`I%MzaBx%|`{UtWKv)%UR;3SH#cE8a!<_J2;WkN1ZGODrpCtWw33*bq}-7WVqd=p zN2z>ZrEc>_?kj{LV1rS=MMeEl)^cltN<~~+S`=VlYg8Q$ljV%y;l`-&a#tKs4t&sv zfI@~$b#XB3j|&oxW4!C2fM(l52HcHEL9Bj2*H zupG}coHz|ldi?$NBanEhBEc^oPI<_qhJ*RV#c*SV3oe;_=8wZGJa$q*^?!Ybez%30 zvI!6q6TgS1ij$e4?y-Z@(RN5!*ry{&%H6{G-jv7|%?G!;2{HDq>3DczfWu>jhkR(j zaSp*13D)iH?H^N70g;k3IyE#Dj)dmB3*tEB{2j{)9$x{;0uvlPc?FP5n z7S>Gt7`kB~0pjQ28n2mzq+~;V{an7?tsS<(>^CvxcEDG^qADgLF z@Y&Z);EuNwdc2(EE(*|mPjN-QL`}&m>{3_L)lbh7WM}xyvD41cL^RbsThl z2F_#SeWt+}ArRd&` zW+(aiHvf_MpFf|%-|Lu(-gpCXu{^uzysqKr-IC054R-U%@|2Lw;;ipSNZsIitin5BrJ;Zt-Rl7~;8d;&h`_C3X2fzLwt zieWUx8+?}Ea%)tY22D79{Q?(HJJsk^c1VF}EHoT?JbfYcCD89nrI>^S4Wi1Wy7hCX zfX%Y`^{6P7>l;%o-hZ}Dh6t61yGZ@Oy{gc0zK4Z1g_#VGR$M(K)8L8tS$TRulCize z+BPWg?OWt4Yl&O!HEJKblXE2$;-B)sValAJEGJa{t;ZDXDl&mCXZ7hCZMMk})Z2Q^ zUx)xJB*|yI&3lKUq@*Mj&E^LXofTRjOI>l2L1bLG^_I4_x=?CBe@1+r+x`U*rbJxU zkAWa~^7t|Ed+6Z|8vYp>&k#_nE&~rF6~`S0+$j>q!(sn~#!B#51UXbZQyy1t{3i=| z5IcAF_fw%RuwIXTYG-FWxKJRw^xG(t+-Av5LP-0OzWzO|xJXm%m2^i$e=Ciqn7!8hKCUDrl5{zh5d8KQ^CeD=`_8 zFOz@r8HY@8o9u~4>%f<1+rQ^3wuw$^Fdzi=W{K-eC7jFnwd3gt%Ldnc0eCA?Mh(5- z|4!@tzld2kP#wShM+@-3@HPKj%0!pxKS-{~kooJ=aW`}P+0U$)hu%;1{^^86Pk-uQ zJXk`76Zz5GvM)gz@K2GE92-L60PpgMGhqbUVrJt#~+uQ$gG3>Bono7 zyaBO7fJPKtOhLxTt|1Mvf4{b{U7VSgI?57p661&AIjVNubk{PqTl%yO;~18C-^ zi831%;D>-rUWO|Gcc?~<37zYmY2n5onR^e=Eo zcw}Tag7Wfri8;*|iiR~C;30ulVYOX(4FFuF$XGFWJxe*43>6Klwx{Pd4-XHw{YES7 zb-eLE&l2*~!ArBmT%)raAR*#8H@7kUo6JD~zF3|I*U70$d@owu zDrQICZQ;rP)x-ik<^Q9q`u})%LtoBAF0M~`m=R^J2WSAc(=_6cMm6zMS0}LGjYc$^ zZF5JKcE*t6FR%F6*!mnk^o%#{Ue$bBiQfb5jFgZOhF4izd%hGduQ()0)#}bkd$5+q zy+CF^%VKBD)zh5)2^kH2uVN)Qh6=4rUc{-g->k6AP*|rK6s&=(x@7Dm3vR(S&Xp~1 zDEPE1TZ9{vraf4J#q_ZvbyNw;f11k&mV^J${TFhDV1~dm8Ay#M1b6Pd0Nn$KYv4#x zKumIvgoNa$S_uPCr+O1Y{}P?ux_$3nQ@&11smqah^*X4jkfb)d@jxP&8nPySK`Y8Y zWDSc576YOZh-#<$iJ>6^VEwZ701xN^2WP5=x;lWGMxdqvsPy&spIjVGLub?R$7Z0vzcv)bj~_n{HN2Xta#=pf zlPCN0%>~2sXu4^5I*Vrako=5puE#=5GGDZ*#3+WGR{84fioJnQgQLS3dUv9zkni#f zcsA%a*G}{8wH%a6OG&LvR&X{qHzRDwO4OTy3qGUt~ssVdwj z8!}jv8XKFFa?`~&RoPdEE6{(T9kiD_EjG_mwir(5u)qBL*>-ax82ai$M^?XO}mXXI)l9$)Y>cm)s;Z}~kVFRO9l8=~j>^ct$7X@oiCb{CTk>uQjy(p1vr2@M% z8KLetYVaa%J7Z?xV@8y6uGa>tm5lBd2sltaVrF4W6)pHt$*YU_{P{C*Cwqr3{Y32N zdc|hb;#d59PDwWCha0#He}i=%r>DNIiKooPA(dK`;6MPegO}$*j*fHjQb~z`(blvZ zk?VegOc)R|@?q0xl{0j88YDkf8cn=z`p`|-pZ5Z_^7LrPlo+l`TJB=CSxeK0 zSTEdtk91UJR4S4;t`3!Nv~nB9W!|o+o$aF$y>9E+H2lnxy0}4`7Ql+ko zG~(iaY&~{vI9H8izuG>i;q&*?>km&Tv%Wxk`T6DZ!OZqdM(jq2=B!D}Bjs-Bq~j&; zhjU$`xE3`?NH&TNuNDdzjW);pX@_%mW?ta0VX=R#JnQ`>x+gRpQt*Cxw9t-+PjjQK zS3u_W#mb$yv7)4;0Dn@Hn%$>V1TE!uj&1{%!2KhhTdv&n2_kvI!SJ;$jE33vTgea? zK{DGS+Nl6)`O|pOAYNrT=OyTfLu4?_+0Im;fnKPuuP?NWwxfy;RA>nKY|qy0NzV4J z%k_@2-sdT@$ULE}UR)*>5t-6C4RuhdwB(oh`jzhTlo($WWhQ33I=xy)qxyUwPhTcR z>Ufd%I@u*D0`+68Kx!Se?B1ozc276iTUN%>@U)IQ8nX2;m2ytK78dZwitLv~TE(-? zWaBW>0%J_7k!F!h-jrv3fV;oH()K7J$Y{DbPLEbOBs{)r|A^{ZUh6Vt_WN!Bdo(?- z2R-`@8ra(B=H{UhO_i;oW*koR;DK@2?2eE@G`m^HPU7uO4ycCJtDN~{$2AnXuy)sE zvsKyO9xkTanvOpJ_R%|*J9~7gzR3~*xTVo+ayL{rnK*vQ0;<4BP6M?5DVG>ett0Ez z-N8py!&ip~oS96loDIwKqa9H@WKb?=9ZX(ds5}C$OhmNh@1Kb%g-j*-?n|`O)9aV^ z-V~@+#{i)A*JkTviAxAp`Fe+ksrmW61`65qs|2E~as#!GES6NkM z<>1iL(C`rgd|a7ke1i3zl?F|+4`oqA1^ltyVwn5}7eCBmG5FGnPCHvW7J~5V_SYv- ziA`vsAYYwMN8iElQ?Ith_T*yy(Nkl=2=C@+tNXoVpfR7UPEnLqb5P#+r3h?Pqz?55 z!|7(y7VaV@*38y-xtEna`KU-T9aM3>>rLj7(a}AHR>GhR@U1*btkp4`cr7~rnFb2} zmC+Pp_@H{Z^*x8-@WvKu%M)W6o|Jzv)mu6AyY$OeEq82p@ec<#4L7I!fu^qPDMN*j zBcil4@6)QGRy-rM!qvIXYS8`OK$%`v_F#If$ZA>2FmlP`oHgmaLaw>;iu>v5Ddh5| zggOt8Zx~!|1aPltTx9^9X~4`O6mpfYA#B8AVAA%vT*wYvOf5}Sonh^6$pTY?%#8h+ zJW0z8&~m^KG&X+V=2GPwSXA^Q;@qM$hGWRpQGsh18Be{vMH2r`XMgFC(p(up@pS%b zhhk|m2--eWn#gEv3xk%A=v*PbU(c;#A(hBgC}Bf~*k~yA4t8wZbN5Hb`O6Bv=}$@w zG$+>Ex{V)A2w0@M#DaG6?*03|Abi={+KL`uvpvx~v1VQ{*D-5qESY);z7J$%nd{;$ zDBv6W>OY8E1W^P3iDXpHPh)t`Uh+AMpvIQk?{QRs-OM&T-G>gWtYnMmVpdUhw%KCM zQGovsr~c;XVI`&TLVJreCyhn&@oUhZN^k5}*JQjY67ZZ*uz; zyx|j)Nk|HoMD6J8-23Nqx$#;1~fq6?8)1cazB&qbRKWx zP#srOQ@KMJA?WHVl(c;yASx!NW2EO-78Ru*rBU=tm-ZHOem(+O((0I9j2x<}sTjF) zu(X{_zl;msb0<8kP^=rk@2{aESllw%2|60CZTaCiM(R9Tc2CXB! zZbIN%I`ht&0tlhc`mE3Ws?r>ah)H=PHLDAkft*A+UJW^hYkU!bMeM-&^%WP;Q7^!}Mi6##Lg&#m!B?-OoouBVqEif^B;hDm6}!Hhj{&+gha$ z?I;-_=s+3Z=;C6Mld+z>?B06uUV)8PIrj;@w2N=kz<~Ffy*&tGwT+CtGW;+cD7THw z@GhP%2dxFFIRHa>jdZMd2y1n!GJ|j8(n2wF#CrR3T6hIiPv^f4YN)RJuR}K|I)Rmi z1uFG{{aeJG3hxbdTgjS{#~_a|<2 zdB*{V& zAzlFh?`&Gd-+81iN@tOP6m(!eohCtlFYmUi3OEtO;6oEfn%_Kd3KE`M39+6}0j!rd zQB!f*Laq89$nLN;r8in)aj;%cX(v|dF_fln1O;Kn6l+- zNK{l)8Ro2dg;fC(Tn%PxV4|bDxF1e`0=*muLpPmB zw`BlzD(7e{>m+oSS+j4SBPb-=1<3JXN}&mO(>-1$Dkylslz%(z>1Qu?&+WdJ6Plc! z*2*-47OXC4>Z4s2?Rsql7R(I|TSFbFZv!j^?EQ#=pA1wq#v|i;Cj5V9FPbVn6MW=o(QLZkqvL6y6?^%h*=52dE+Jp%Wgb*$ z{rtx_@$ht_Im{acw?9}g(|xU<3XvflC(Nv*3RJ(}Q^UBUK3(xd`@IE*CVKI>*o7{_V|z0ib-d(dv(&%zcUNQZ{ndrFHxS_WxDI z5TyLk!p@-5Qpe`1_@j^Ex93LxxN*2RIv(o)iJe76KtP~6+*@I{uC$hf!$qMHYO)rj z^ZtDu$jK74Gk29^rRZdamYla-R>~Qm$K)f|YkD%O)jvl_fL@Vm?-$tD3d4ghk+dlU*SZCaIOw zFjB1TcI+01d&Ir158rR-POEDOkwDZo$$Z%EYEBcJ_$%US-X^b&9-VWwGr zeu1=|G(gArE^A7elE6EXMPKEyZ4J`P)vKLT*fNo1SG-F^?o{JwhD^qg+%K=G)Ff6; z3$ecje~DHzfE_;WhA)}#o?!)u5kZAb9BQnQc6yM3`19wPw?=8>VkYO40ia&b(-nT_ zH;-oENI&4H1#&3($c3SXf3PF#k=cZ5PqTFOqc=dBJ*8)9|DTQb{hNq-y;d+`W3E=w z0pGq*Ty*6btH!q0mzrdgZ{CX}qI4zLdqP6qj1(XU{M^|NgavdBOH%zovibpw8PtD~ECGR()MvlDaq7t3&}pXf}$1Y=+Bv;ilp* zg@3eSjUMq86)t5MyXuQ`51fHFg((c-`1SkHszyrlWYfM*yPpNdaAIDv1cQ8TW_48@ zIRmxRM5yvx7Xyfs=mUYPb5^(0{UiYOmHT~?Fe1boW)IB=$HPaD9>Ge7PLC>SIr;Xp zvgpF;RO&&hA(?h=QF9TFOuR7W#=8!E*v_-LjFvGK4%;yD@_ek=EFdO^7)B``v9sUv1f4=pnj|4`;fPnSUL}I` z6FCtAl2&Wbl0t*E6@*Q@p(a2fpP%eOgRilx>nli6dWy}|>KhtRMhcDaV5Y0`crHL8 zS2GUU2lF#CwSds}%nTG1@FBu*L%Sf^V!9gowl7WG0IKCypVA=TgIht{79Hq5n2ZA9 z%p$B-bvy{h21Nr1fP=1sp2#bBP!PIsAq9Rg2y-JocVK|c{@mF!+r5V1*rFvD{gt#d zZ5|fP$BM!-RC6~sH}|AsU8X`~1alQL2|!G^4DxLlhUMu#1x+9{C&U6Du>T`2J=~lJ ztpN93fB*jd30Zrk(_U*w#{w7=K(Eih`8k;UG2zjdwSwn~{osq@o-heXmcFkr~?Z(4^fn z6Z>@QN1>fb-DK;T$b`$c7DUncx8ZuGl|NPq1G>5twjhvbIRob5C@( zlXH3j^em<@5!rnTU1F>CA^IOuQ9_{W&CAb!4?-hQEkm*C1(T;>3<(a9PMPHDGKPpu;2BmEH_zYxt53mI}jd^`oOvV9CIKLm zZy*@|Z86Oc4I`BM&n+Lq=t~kviwZ5K<01YaCHz7`nZW_97=GfhH@PxZhQ^AB0@Y(N z1)jmdeROqoeG3hw_(jN{Ac{t{u-IlX9Gnk}A#=_FsL}&!#p>fZ%O$(#5- zKGFhvW*eU@v+;qcRet-IE-&cl>8%ee*>i5rd(VG-)fk>x4vn>TK!I=JP zIjzS-LP}}@#*EUYy1qYE=G#|?ZAWVC!L3Zf2Xi!HpbG|(Lk!f4D6hoDDPX7%FRQMu z4rztR0Zx#{LEH0VW~NEf0X?W5f#qN7PRLc!*VRqMSbQ1yzzU{0(QwJKR6O8wt_H$m zV`Itb>Byc0U>^iX@P!L5!RX$qs4%~)MRfiKIY>4xI#88ZB+tKrMfUvGll-NW{fkQz zp{%T|uA$+ySu|`2nkCS|B)@O?{D};GGzjHCS^$F}bJE`aH!D*`I!WWVC`6p_@8AFF zw2%o(_iuZTOJSTd7^Hem#<4Xu0?;=&2?sq!yk1B^mO7de-X)6&vHwuT`hND(=&mK9}XgUBYi2{)wyFjyNl`rbi~On`HDni+w5 z9>HeVBgmTmr>^eBdY_@w>#ffdHn0mgZ{BQ<4ui>1QBopj(fX=f9OU1;Q7JUMg8)T+ zEf{@&02znreRzI4t!ffaWa|=!wWeIQt9b3~)&~4w$Sn^Z2PQ4M_E-B?fh(}6Iw1Jg zPkFhL5_Al6!!Qs_PAwBVQ?Xs$&MLAmx2TL`1gRQyK-PpfLjF5jZw3vT4mZ?b`7DiR6N&Bj7}k zU}8c-Z|}Ea<>WL#Ho-K{uEs0&{yuFNzL-DS7{wzPeRR!VQ1JS%8C60-H3hcPLjP-g zN&!#p7;gz=+o589!VIeWR7^ALiD{UJpWPz3xte5X%%@oU(A<~m=1pBK{fmn`2C-85 z-=lOti%+=<42RK1IYlhLr<#p7a7rjiO%;JXGd##lO?^yswrSy!+14gOuU^VDs_5r?L@sEcsH*x4#-z}k^`Ms*qhY`g zS%Ltw-MbCK{M(-`-;3Neez#Zg*p#UQs)*5aKUvh*I6!!=EG{nI(Er_?AW+kk%zSck z;?>mfXw_6_^|~{m&7|Eh$N=3=J1yK(V0@gaf*aAJ2met6S6=+9pED=o8aKSAntEu^ z5#IfJ)J-S|&+R)a2*iDa7Y@P`<@W#hM?oRfDuW=BGW8r00xy}iMe7WMpjcgHR7828 zH`-}ll5nBP(Xt=iwYY(t$pn*;p1MCdXb4+k4D9`0N*|-;9N9v*ZprW`YUBdV9`7G!d{Yb&$$Ej8&t-pe48oc;Ym zU1fAcd8&-l*}(ojTO7Z8tPHsTL$4?bd;@bkDFT7|um=Ufgt+#9{>MD3iff+18>44M zZHJq(_Uz?fF_(_-Q00j zB%95#a68)%pVXIpJ4zKI?oz`bJ92{JEtZH1AG9FnA~^(lDZiJ9t0!nRyW`8gcNo2& zAo_Pt1_$Jd2)KJN9C^t$1sq?6DGq*5-N=$U;QJ9lM<7}9lw7do&Whl7b`I{Ms(l@J zLG32xmrvTYIHa{0d7YM?Mpbw|R_+|Oby+BSf{x>!S@~WVkDCqg(0#=k{W~3-)Wrkh zxKn6~-Bm4aneU}eaJy5CyufYuB;Gq5_o4eu+lPKMnZFd(l*z4J%rx8*iFt_@m6a5! z3tbUA!tekCs*$HpSYDeLua8YoM{{3$#ea?TRdVRl=Jf2LMrk|0rHS5wJA%8g%Db?f zqB7ZBGA;~S0Up|d!A*jIC%^uE#Ur1F9IvtqN^|naae<_Qj{bgm_C&1&-|-+Gdqly6 z7sH44AM8k?^w0q7_ytr>$N6&SwAN^oomm%)=k>Ei_(bh zg=2dS_4J_E)7|YFUe8^(O(z4|e{I)Yt~iE4PPRfv{-LN%I1iO5BIbzckrf4D9UY%` z&X`UNEpjr!b>yStkY@BOyByjd^AZGzrYCT7EY0J%x;$mzloz*pZre5z!tF|i*lbCC z{;z}jCQ{|R@RXiCtznrtN@|lkM|N9tbL|(dp3F?uB#MZSAbR=D%)#h^KgJv}^bF+u zh{tGNIFIG%bXH&u5BRJd)!T#gu&R`UNCNNM3#qrG-xk;pK|#Dj6!v&MZtHkuVV73= z?ir5QM0fX_v>E^fVv!)TpO+0w)ZP`o1vI^bM0x#qrzkJOBK$|CO=m z0rDqD^%|UiSprL6e`s)g@X$e+fbB^t?ScI?Q8k=c7)!GPmbK@+X|fqh23<3R7$y9!jPC zVE)@TnUHZFgNniEu|2#+p-Ue$1XFE8F&=Q@fK3d<^e-Y!m zTSGHWXmz&Lefao4&Y_^DJiHcJ)G{xpr)XE-X0bj&79@rp7>EIuc#iy%!GR%giASjH z(Kf?FgVb67b}IbN)PgVjh2CN0on_z&cuM@9#}r5tcqMDX&O~bO0?kwd1HHEQL6x=A z&Z;!5li(l_Fur`$4C zkC#lXcD^Ban>g(>98ZyJ?JI5U9m_MH;(BSSH!;h|RdTzgK4J)HiR{!5B;jZK$75ch zRS)2?c34%SY6y zCEC(CGHUs9hcV!Z$ss_>>a#O=_ZQ1j^?OcQe99`5VH^8H0QhyXPtPv-sJ||hT zk#cP)`Bnb%UYMfXW=weWuc<2M#v<25T0f3Lo?Z37#g_EDud;1bA=oMo4-`AwguVlp zwZh$R`?dD7eS477M^LN;U;2JYk*pXhpM2z5Ldqu`gdnXp_TG>XZ!|7S-MH#F*@Gpd zBd<>%-9OlNk?L!MV5fy@b4H!*n#daP6Q_*Kgu(l@opPg3hbBD(_ z46Y*j7nMHt(+6={QILHB8=je$6|guuUGKd~mVkM15??j3OmZw!_?|4sj*OR|?7_l8 zAOmR{3GPGK$)7oXW~OxU#>VU4l{k8$L{&wo)Ym7bX>f6D()-g+Q$`-)io)}ir@hUk z#^rpuwRf_x#A|@v9BZjCdPOhf-kntqQ zm^jwj<$FI3?#o2f&4nvnvhzjjvhNz%3~5P8FAfguRp`QN31`2iWM@nEeDJ^_qje~o z_+@5Ec96gO3i5}9c?r+o#U01`;^q_;7ozDsM_ywP7#_xsnua%N2M-!!yQ1viZ{E#H znqRzpcm#j7Er;cqTU|J^>_l8EtY5VS5cg-%`p%HppQG%AA5%C5qjZ^rG%=l26_fD` zV|734i`xEMd*2<_RQ7h8u`45t4HYR4pbQ|O(vhZ&C?F`k2a(=eXrV+@u%Ix8CLm3k zlz^1bLa||_N2>HDHB>190?B=k^Z)Pr?)~ol@A{w*5^{2OIeV{nz3W{&bIEc6nzcA* zW+@6&pT)J@^u>!8o^nYI1&1;gZP~6&VC3{oO@&E=qK`jaZQ`0g+LG(WNO@>iOs_-Q zIGZ&4JY{h+9928+L+YYX-plw2OtJYUwqyaN|A5Qmgln$>u7YG%kYQ%QEy6fTnLqNw zJPP;MXWJ4%P^;H_QnKt#rx@f91kXq^Uv|?uLxEQ<+y_G2=j-jddXl|rU%=9YgJ_(FmhTh;$LryAdsh;tr7Ndm%55T%=t+tqz1v@vb+cT- z#ph{4e}@C9Z|498W4gAs_NX#sBT%smgc~G|>5RUTqh_gjgEMoh1h1!}z8I^gMqPv( zTCtbK+sA{^>7FuU0jCql<4>uZl$uATT&?5F5-$=3{63`THmcXq7p^omoyoD<&+6PG zi(DS=ZXqtK*F~!LG+C3QK=(!|uemRGpB#+qPrq~LF1FV!lK1i?(`9o zyVYb`T#mmyw|_OdA^H_}wZHG+k)Dfj2QaCpOM3G=JM}WNvKr@o_Pe%Ts}P9mb!HuS z9oj9^C!Elv_t5Rj*s^-oo?9jHzCBzfbqksA$5}+TwI6>nzd{D;13n-rIh|C*wD7udt$3kX;KAf+#p zb<)@jdV0DGgPuZP3~?2M%s3^fHzccslqHHMdf70^r2hlqu>kP zI1-SN64ENVnswp4M&S9+3fqYm0`kr0c5%rFZHIIlgi8!y+tXOkkA@0+ab`=jn{A=p z4Lo1lX``GihD!`(!xIkM15Db9$l=9Ep9P9n8{^L<8SP)K%D#4U*A0t`fOL9b?IW|0 zh!+wfYvu;aBU*EuIV;~KQH)@Y@E-fce zuuM1Z_7jE-ezlHCc{V^5FZH=w0XJ##MYx<;C2y9@@brsm7)zT`)jA>Uc5<*sqz8{oGHm%*~Z4 zr^aZ(BjqPgoDdWfyA73G&u^^B)s&$lTZOu5T;?hj2H1`lMxA5r#E}LM#vR&zQkUa5 zX$xFVSlCuNGxEuOWwN%mxj6w!V9%2{wBs+^sF$#vDjyCefpr7N+A^>eTN7fq(|*<9 zvH*@E;!A$O7-L65_QO(jI^~eBXIkM6I}BK90LdX4Ac}mbX$3oW`iD$n%ewh1)7`_g zxOsj3>rG$OFTc~_jCcm72xeeY(-UKe&Vi&45FSVL4GbJ171fd)zNWO+p>>7OJIBY?eLHuk(3#PafaGVpP)>UmmB^BMSHi8a>j4zs9#rzPExc5TL9dXLU;3RL4t| z{XaZqyQr^fZ!BAUJ1TdW(Qk-@8c1hdNGUY?X5Y8MZ4{DWR3@`#S2LR3zhiVMJypNucN|aycBB6*lobaFiDQNy)v z>m;vM*{wF__HAf}jzAJd=WYtpIIs|LFBylr=$N>;?AwZ)f9o`f(jbrF)LU$W46g*_ z#%gxf`|%l&>6?zL+sXIpryGR}J3--* zIN#AnM??N{`s&xFrjEkkHDNUo(m?uVP4&nFmQu`&jsk7Ks|pJB)*kX~E61U+4ta~m z@M5cYB8B>GM`UfJcY*b>iVw+LP*4z_4p$XMgwG#dRYTMqxLbd@7XTIYB;( zu;4rXLfR2%MeKCiOqBc)Zyp#n+iLt9dhtj66ren0;#dB%}twzIPZbn z#Q@5iiQp5e0Q11n-kt)v2n=L$)Y>Dsu3%_`zSIXSq7>o6>NikabQS81+JV@H=YoA* zvZ&V?p8@GlN3gy!A^8x1dwwHSSX+xmuIl1KFd&#@+NA^;=SN1hDSioGC%Jz- zXi|eEqQR~~^VP+x<%2$nU|h=ey^u)T`EtgsQ>1)=R}Bkyy+%=CBmO1OH>{D@mD__3 z&^&SC#8yWNBLm3wnTqj_rC=r^F>Y zd${ItN6i;x$vK}y%jWMNDvYB#j}HNlEzlBk*0rt8HXOIEKO_>nAmvK5|HccHzZU-2>IBnQ7e+L1;`%u3 z0oc`VH+J*fH^?_i16jay=t(rigUlt6lVigKs(&WlF8Sc<>qm_(EnQF{F5w6EbpgI) zE?;}7YTF_!G14GgL)@&TGj(H+FDpEopk0P!5-jooXHOMWj2&_rsy2o)=&q*%$=^qN zSeK8@WQo|87^4e8;52-V%{Q=u;#+C9<=AGY-u6{55*-x1HMnT|=E$^6u4~Pj!47N? z-(Vy@`Itju+Z>O~Dv1CFSkT0Fb{^&wrHHVHLvR6>3R$q~+<__bb2BhEC+FavMVPl) zkd|V%+9)5wI(4oo!GaeP6_qYMQ#M$!i@xtR>xn$tXiOCexwaP)vzop`H^idfC6&gT`Vw?IgF?DjHw4JJ)qR#pzF zgSwXc=FN;P$FkqB)n*N+l(SJucM4PGL7n1s_4Z;S?w0vh@V;vD_`yfmirBy$bWIF>MO8>D;^m9Wbp;bfGaH zbIR?8e5pCYub)}3wz08E0M{er!LVR%^>Bbt3-B^5v;yV*B5EPaR?N!2PmaXdH{qh~ ziEgzy{wtF?Sy`&g6(wdmY)0AUQ(mM&WW|AvHy_sS%=$8rGpOUi0XHMgBG?!{{dF>q z;lHz9UZ)Rk91@VDRn8N~k0giLH?7pQrqvwh@}KAJQgg%(gSulyS$WI91`P{qS6A0w zlaUXMpK*255my!{Ck)NfOxx;WOyd~>qb+OZk!7DLX7Or>Vf=GqRXykw}X1%Ne^zJ*00$VaM; z4TG;=zkXI8>M#Y>xE3xa!-L&jV4jD}La?FaX6_-jZOMc8 z0+#&NAOpNM@{mm(4=W!5a)~n^_VaXtZzTry!Oz5M2$j?{3j@GH<8FYxu=s$CwaOI( zN@HVVJG_wyg_iI7`9gn$9XgwftIP^)Ll)pWo;x+@$XUl+lfat?3O-5iXUiMDq_BI6ZQ;lg z&VI>4b;?~P8jt^&A;V9G+O9(A_|j>uHD5L+)q?4`z6~lGv69|_H!R=VW-#hHCCfiR zyf6#KPzSePa{3NvX=4@V}x{lfKuMVOf z*m0j-bY$b^`1#m>>4EqMjgNAi5$_Gb+7I3~0gVUyYS@r7Gcx`X5fNGX&h&D0biB)q zj*ebD-l)m~Cmu}0xj1`BhvR>3q_9IHkOo{s(h<8~1dY7bveO%%E{dgn z;$hs8m6gTL6_F7j3ZBL{X+r^SC9#E_5@FRY{Zdeo-Y~V|01#;lrUi#?P)1fmFj&G= z&6oAB;+QlXOv{clZq;dd1y$P%z3>Zgt+nt?7To1A_K^?IRGzkO@o$y%YKYMma zmNVk*Fb&r1y!wd%9Xd|z8L>(BwBGmFuOis=k)@{YuHot-mLv`a^oY@;>;2;uV+hg_?tl0FBZuRu^#DJkOaci-9 zp^CbS?5%(qyFevjx;=&aa@uYhw%Tv-2;~5_Xrit;f+WqP*jnxGg^jQ?uKz#;0>-WS zXtXG4u;7@M%vC7YW($1v)Iee)Gdr6KCW763k@9cOxK>;RwOrNDM&YhH9FBmlk#jG& zvQVJ{Mo8T@apvcNZxe5m*F_xH#DbSufXjJHML-17Sj4ju!G?y0*^C`J6}dey89cRI zk;2@bg3YZEZd??|mm~n5Cg8b=4w#n|0fQt2!~s<7nxqE<3UP#}1ELKQW-{Jx7TV-s zpcMe4xc12O?w8^D1s)AHZfP1bnNIHCDH-3f9PX@Nh-tB9*`8w_n)jrokf&Az>%Scz z_^|d9N249zAkXd8>^X1oTwJnt!!af7_up~7f1s?u{j?JLe+?`!1xP~5*FsgT+}-!% z_U zb;I`6GK+8l6>1*93bH2F^jcF!ind1#s&EH>B}aLml-2@^=#1a>VS&ws*|(S$R~=gR z^C1s?6IT`_JtI;fnnqYV#IlGj<} zd9XQV?x}Q@Bz8Fzif{mZRdltIvhzS?@LrKb04FwiM3hchF|9Ryb$)f55Gn`pyz=pn zBFe|MUNij#1BDegHb+cL_c?7{yz5%TxMp7-u;gn3iTclHb(|F1gStM222B4f9yPik zVmh6+O$cCaBAm~?s}QTHlJdSREr@OzEXd=mb=QriB3064bO7^bse>g zZr_S`thB%ij}3Q`)ow%u@LAi*>c_V|)wtI+#ModNpn*^akBZ*!H({LH&?z-))da%A z{rL5cAa2dn$7Aa`{~Zazf2lF}PrvY)bDF)x*fmn%i1%+mNqGgx7rz3989DILpleje z%(Kd~C=^91yW<>bkbTAD(*CMXt}#v5k%9l)y=uVoJK++~Nw3r?Ynxej8~x_JAbcp} z%E>p-GO%qX*Kx05 zv$kyAqrfzw8p!G%|Ia)5Kj=U16VnBjutB-`x!UPX=b)b>d(K2+A%SfF@BR8D>MInB z+gy&9!zf0zO|1}VBME(2u7Z4>6tseM3D9Shk^QK%Cft#RqUEOaI)yiASz|~#y3i{7 zxTd=#iIDDTY-n`$9Yvv18(ua#8WY%Uva*TP4IjxIkN`}aZ~hJSB$2x}-jk$|o0I3( zvkZgWL+u!C6k0oryp{*A$zd|o=V#<2E1P!QugA3O=thsd`!%5U=0WMYhY!D)eaU6| zNp_OGs$(Ly%kk;jMHK3fy@_3KRI`OA$M3OWvNhOQkmrQg=mNWJq-ttFHQIYlT^4@p zshVR?f~c9yf1#84Pji@Gk`kntf-ImduP&*;ISW0oGh)77KZ(T8xo_V-nK6UXg>N9G zJgV0*34~cLXmmH;M(;&U2SPxp58i%rjuKXywxfU~BEgF3t)On!G(eo#LDwGeEGL36 zt7daDQRu>j&P!y@sAi#ED9W{~S3|(S7|{beKv>yO5Rt)OMzIK5(}!v)ZG;y?0;zDZ zS|bFmE>y2*)qubgVK|cg#-3X1v!U)kK^QDZ7eJ(i;Fe#P*tZ34ACh39xAQ|l$)RZe z(J?Iw@)I0m`@I)f(YAx7TY;{Y>~*`>9$|<%UQGZ^Rlcr&-T>|gbg^&1NO>AU$s)Ah zXs9jywRg8MLJ_cwq4NPD8fGVBbCGv}vUK0NKXZo`laf*h`=qgM)~#;{$|(QK2@K+@ z;6xH{WEsW@< z)O-OQy?1BqWZfOWGW-r8U+t^($_{4IhpE7{{XQ1}0DBq~d2#%Rs$ln_BxY zV_h21(rhh#p#v1x=hPw<)?dM~6V>mRA*M$h7{OdOIx31!tqSP9H4EmN1s28&Z0(N& zYLJwWum=sF+|+BYd%-m*R8o{uC)if!ym`Z`rr!4otFvp+`m*E+*B1pFFu{W0_!9(U z)NcROfOmeK@KnPCoHX5ob#TZr6ODs0&9`m44HOcz)(|=dxGR1&P@%h(-S+V*%gxQ@ zS3?+Afb+X8Mb&_r(uc5ncchC{BD|YBc*8O#Ha54a%0w|m>`wVz=;;@;!z`$2xoav4 z;1MWDSj*Uhh2~gaj$eiOt4d@d5esRdN&{g}feBQ!v9_LuusLo4$f$Spp6+gSJd^=- z&H3XfVB&cP%+>My0A|_AbXp#PaF{g6v{c3;KpRYTX27VY2jq*L*|&KBg4q)M>X0LJ z7?r5|qF;xNG-KhzvDhlr2+kbP<4$KKy5a|2-HMQjBMA5pPC~H&)cuRl?RO=KysH5t zwz18f50xO$gg4~ZuLFR)q2E?oTKfL+!@CeqholVqjUR^C8UUNb9}HlJwUc;Ilr`uD zM;Mjdf&v5JKia>vM@cmW!4LY%-BN%P`gX4$rtBMp9zZsvbfel8qKflm-U-PF?J;wqz_GBHb^!U44;ocdt!!P0QkwOcdatxzatAK68b?p9s_aHEQ}n&jGn1* zc7dTsFmyp31B)Q`Y^9wXWLbmq2P&Xgwkwqj5EH=T6&p+=4bMQRKRZY&2h#79$AAKO zEpeVWe%u~bZG^G$^z!ls-V$LQ<9etu{7I0;LDoMa6i|Q+|K?P3Kmm8a+$F^25}__3 zVHF)0cQWPV?hJHL>0~AWRU!J>g&d2l@4r3+hgRtS>ADg+BbFP1P3*-0~x4j>I_KIZ}3gvRe zui~1lar`A7JK+6ljo5N?2ytYpwvVl9^WI&k`%6N(j*zK~ei_TLQZ%118v8O?sH7pE zFQ#i=mJ@|~B%@<2l9w4|De65^Ol*EG(d%pwWL#!(_=VQ_#@|qX@I{jI!c?Y@-d4pxSpn$N-`KTO~DXKek z%>5r$F@PceZwNl2`~QcM(f@yi|B>SM|99ViulwF_)f>|B<`Ae*uAYa?sfN!vc(Td= zsyP2cSn;3WvQdRU;Wlz%6N&1blcrPa=5rH&Yex&OQ*6WS@$$kaF}!Dlh9}tA z1R%7wq*E-v1EeS*Z=vb=;x50rFhzL1ai_fgCxagPE^+6>0`MHkb=ykuWHcYOa^-*z z3eLgPehI|oE+sJTmTr)I@DdZROk3#Vr#X3LhUHF2Zqish;lEUyy_X{^WI~9$@r4x? zM#u?EBObQ4Szs1kSY2Js-+d^!zP^5IN{M;eZyRu*E<$(0fdP0U;ZtgH6+yYQVW)}U z`M6POTOQ&5F6KF1^)fRtw7@VU$^~WRVcz3RAT}_B$}hZ=b>QB; zdjZeud3$?*pNFFdD3B|A19T(jM8Z|Py1IHP91Nq}B(LY{S^!maU+E87sjDGFj;}O| zUthT$yRz_-zfyXDcX7X%>Ojv{$2F7LgF0qr`B0Yl7XI|E06*3Nw`m{BudJ^h15hRt z5b<>)^J6HBB%D6Br{~azXV0D?AM$p6V(01&?*-+;@4{2!Q~RoVHG^(6cy4L_l+MMSy)cyV`JLdqC%Joe>UT+?YH3nT1hUYnRrge1HG9giBUo@N(Fg0O{JT0gleZ=bWcOIZnm?IHhE`Q`g3EbqxxP&kLd_i~i^|d?f z9xQRvZJHXYzTHYDH*Rixe?m6as#C-N=wGQjBJefzinNI|#98mGh>*POJa$_?4WbPv zaG*CSx)%yXOsO!ue|rxh5g$}NEf*Aw5(O@7!z|Y`FvtSKmp>K`f<6i#ZS}3jyANhN zR$)8rSs|}QRcw%Rs>aYtYe!ManIkUW-(3Yx`i`n<7{9~D(x{NAX!o1z;d;8dNb&9r zgbIdmOitqXOfY0Qk%mh-fB)xeF5mAsu?C1=BrQMgAsBtQs3-585u?eXP&?pSjvN$M zIk>>y56lMuLsNvF9#u^-y;3GVTiTOGe?Y1)iHx!NT>rDIeKwY#>RU3XUt~BGIs7Eh z*=n_?I9&~y=5WH|z<}8acJ|zN@9scC23mc+^S6aKh!8=_;sA#?9?T^y!^tA@lLtM5 zD=RBouBAh7BM%ZZ0Y>?h#bZl^Q>P8%>Y!IZd6I)9yb-xWM%Kj?X}4sEt^ZPkLtSzq za=Z}L&^cL@q@V4QwmTI7QC4Zl~WMa;@;M$KT_m93ij%#WLEQZWF{YgfU$SC1eFFo^r1}NsimuZ6e4tS+ zUo7+T^7QreG9c&#-hQ8`aX-|J6P0kmBXUeQKkjXAuDYb8q$Q0@DYzP8 zJdh`tSj-vHerb7nbVNlz5Fsgm1}HCuljfi-RNa6|duew$z7*oGK_D$41r1e%4|k1E zxp4Ixd-ri9+%V&Bi(}e*^Efe{v5z`)2eiA1^U9-ta#4 ziPtH0g(|*~wcZ_-v#?OKpoc+riO3}YeurEMSPl^?g3V#fv;I9)qJpzM|M|bZgmgJ} zK~(h7RXqL{5;PCbqOA*pDGT;$roHn}b^O6+ zPt>LkV}g9mEBVC7d}xWxLWk2vbz$oMj53*@2f5>r1+$8Gz2mU6l{!7>hg!i( z%^%wT#Kd$J5QpF--nQ=|lwY`5%ja%&=oEtJvoPkW{YWF5ITnG+0;hoY|d2h?nuWcX!^0lZ(+K7kf7nqrz3%!o-rDGNqZC&+prN z&yon6MDN?H_C{g>*CvZP#`V${a$qi;J9iF%#NmMO8#qJkl8qDI{-uDin%YxX&v{$X zZC}6MfgSp6Gn@y~{pQ9#<*A-?yUZOOgGkZ??PES~T|BKD6Y6+#8$9p7L`VlXuhH18 z>B$kkUB_l%+eUJ0FuY&B9lN`|u`D7g!g=v7O8V>70e~NI{Q7e-^!eek3W6j5#qaNW+_rG9F`Fkvk5?Mq7r60;|RcjPyZhRvu&KNz@H^(yMhRa=TK_b Mwf-)+cK5;m0*p}6zyJUM diff --git a/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png b/src/core/features/login/tests/behat/snapshots/test-basic-usage-of-login-in-app-add-a-new-account-in-the-app--site-name-in-displayed-when-adding-a-new-account_9.png index 5d1a748b62a008a46116b9cab7134a1cfed952f0..9268fab1a4b2659b3947d66f03eec28136fc5227 100644 GIT binary patch literal 28547 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℞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 Date: Wed, 7 Feb 2024 12:55:12 +0100 Subject: [PATCH 15/19] MOBILE-4304 behat: Fix core file tests --- .../tests/behat/behat_app.php | 32 ++++++++++++ src/core/tests/behat/open_files.feature | 19 +++---- src/core/utils/types.ts | 6 +++ src/testing/services/behat-runtime.ts | 52 +++++++++++++++++++ 4 files changed, 97 insertions(+), 12 deletions(-) diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index cbb164b329d..15e4b0eef44 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -936,6 +936,38 @@ public function the_app_should_have_opened_a_browser_tab(bool $not = false, ?str }); } + /** + * Check that the app opened a url. + * + * @Then /^the app should( not)? have opened url "([^"]+)"(?: with contents "([^"]+)")?(?: (once|\d+ times))?$/ + * @param bool $not Whether to check if the app did not open the url + * @param string $urlpattern Url pattern + * @param string $contents Url contents + * @param string $times How many times the url should have been opened + */ + public function the_app_should_have_opened_url(bool $not, string $urlpattern, ?string $contents = null, ?string $times = null) { + if (is_null($times) || $times === 'once') { + $times = 1; + } else { + $times = intval(substr($times, 0, strlen($times) - 6)); + } + + $this->spin(function() use ($not, $urlpattern, $contents, $times) { + $result = $this->runtime_js("hasOpenedUrl('$urlpattern', '$contents', $times)"); + + // TODO process times + if ($not && $result === 'OK') { + throw new DriverException('Error, an url was opened that should not have'); + } + + if (!$not && $result !== 'OK') { + throw new DriverException('Error asserting that url was opened - ' . $result); + } + + return true; + }); + } + /** * Switches to a newly-opened browser tab. * diff --git a/src/core/tests/behat/open_files.feature b/src/core/tests/behat/open_files.feature index 6e4fd1f7bab..fcd8c19dc28 100644 --- a/src/core/tests/behat/open_files.feature +++ b/src/core/tests/behat/open_files.feature @@ -35,25 +35,20 @@ Feature: It opens files properly. Then I should find "This file may not work as expected on this device" in the app When I press "Open file" in the app - Then the app should have opened a browser tab with url "^blob:" - - When I switch to the browser tab opened by the app - Then I should see "Test resource A rtf.rtf file" + Then the app should have opened url "^blob:" with contents "Test resource A rtf.rtf file" once - When I close the browser tab opened by the app - And I press "Open" in the app + When I press "Open" in the app Then I should find "This file may not work as expected on this device" in the app When I select "Don't show again." in the app And I press "Open file" in the app - Then the app should have opened a browser tab with url "^blob:" + Then the app should have opened url "^blob:" with contents "Test resource A rtf.rtf file" 2 times - When I close the browser tab opened by the app - And I press "Open" in the app - Then the app should have opened a browser tab with url "^blob:" + When I press "Open" in the app + Then I should not find "This file may not work as expected on this device" in the app + And the app should have opened url "^blob:" with contents "Test resource A rtf.rtf file" 3 times - When I close the browser tab opened by the app - And I press the back button in the app + When I press the back button in the app And I press "Test DOC" in the app And I press "Open" in the app Then I should find "This file may not work as expected on this device" in the app diff --git a/src/core/utils/types.ts b/src/core/utils/types.ts index d0131a35e0a..30a311d82e6 100644 --- a/src/core/utils/types.ts +++ b/src/core/utils/types.ts @@ -23,6 +23,12 @@ export type Constructor = { new(...args: any[]): T }; */ export type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; +/** + * Helper to get closure args. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type GetClosureArgs = T extends (...args: infer TArgs) => any ? TArgs : never; + /** * Helper type to flatten complex types. */ diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index aa4945adcfc..ea9ad550d04 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -31,6 +31,7 @@ import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation'; import { Swiper } from 'swiper'; import { LocalNotificationsMock } from '@features/emulator/services/local-notifications'; +import { GetClosureArgs } from '@/core/utils/types'; /** * Behat runtime servive with public API. @@ -39,6 +40,10 @@ import { LocalNotificationsMock } from '@features/emulator/services/local-notifi export class TestingBehatRuntimeService { protected initialized = false; + protected openedUrls: { + args: GetClosureArgs; + contents?: string; + }[] = []; get cronDelegate(): CoreCronDelegateService { return CoreCronDelegate.instance; @@ -90,6 +95,14 @@ export class TestingBehatRuntimeService { document.cookie = 'MoodleAppConfig=' + JSON.stringify(options.configOverrides); CoreConfig.patchEnvironment(options.configOverrides, { patchDefault: true }); } + + // Spy on window.open. + const originalOpen = window.open.bind(window); + window.open = (...args) => { + this.openedUrls.push({ args }); + + return originalOpen(...args); + }; } /** @@ -274,6 +287,45 @@ export class TestingBehatRuntimeService { } } + /** + * Check whether the given url has been opened in the app. + * + * @param urlPattern Url pattern. + * @param contents Url contents. + * @param times How many times it should have been opened. + * @returns OK if successful, or ERROR: followed by message + */ + async hasOpenedUrl(urlPattern: string, contents: string, times: number): Promise { + const urlRegExp = new RegExp(urlPattern); + const urlMatches = await Promise.all(this.openedUrls.map(async (openedUrl) => { + const renderedUrl = openedUrl.args[0]?.toString() ?? ''; + + if (!urlRegExp.test(renderedUrl)) { + return false; + } + + if (contents && !('contents' in openedUrl)) { + const response = await fetch(renderedUrl); + + openedUrl.contents = await response.text(); + } + + if (contents && contents !== openedUrl.contents) { + return false; + } + + return true; + })); + + if (urlMatches.filter(matches => !!matches).length === times) { + return 'OK'; + } + + return times === 1 + ? `ERROR: Url matching '${urlPattern}' with '${contents}' contents has not been opened once` + : `ERROR: Url matching '${urlPattern}' with '${contents}' contents has not been opened ${times} times`; + } + /** * Load more items form an active list with infinite loader. * From 7adf75f4901368a4c96582685679a67aa75f176d Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 7 Feb 2024 14:25:52 +0100 Subject: [PATCH 16/19] MOBILE-4304 core: Improve infinite-loader race conditions --- .../infinite-loading/infinite-loading.ts | 12 +++++------ src/core/services/utils/utils.ts | 20 ++++++++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/core/components/infinite-loading/infinite-loading.ts b/src/core/components/infinite-loading/infinite-loading.ts index 87e2125b7a7..d1ed55b0ddd 100644 --- a/src/core/components/infinite-loading/infinite-loading.ts +++ b/src/core/components/infinite-loading/infinite-loading.ts @@ -69,17 +69,17 @@ export class CoreInfiniteLoadingComponent implements OnChanges { return; } - // Wait until next tick to allow items to render and scroll content to grow. - await CoreUtils.nextTick(); + const scrollElement = await this.hostElement.closest('ion-content')?.getScrollElement(); - // Calculate distance from edge. - const content = this.hostElement.closest('ion-content'); - if (!content) { + if (!scrollElement) { return; } - const scrollElement = await content.getScrollElement(); + // Wait to allow items to render and scroll content to grow. + await CoreUtils.nextTick(); + await CoreUtils.waitFor(() => scrollElement.scrollHeight > scrollElement.clientHeight, { timeout: 1000 }); + // Calculate distance from edge. const infiniteHeight = this.hostElement.getBoundingClientRect().height; const scrollTop = scrollElement.scrollTop; const height = scrollElement.offsetHeight; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 7b806af80d9..312dc74d370 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -1826,23 +1826,29 @@ export class CoreUtilsProvider { * @param condition Condition. * @returns Cancellable promise. */ - waitFor(condition: () => boolean, interval: number = 50): CoreCancellablePromise { + waitFor(condition: () => boolean): CoreCancellablePromise; + waitFor(condition: () => boolean, options: CoreUtilsWaitOptions): CoreCancellablePromise; + waitFor(condition: () => boolean, interval: number): CoreCancellablePromise; + waitFor(condition: () => boolean, optionsOrInterval: CoreUtilsWaitOptions | number = {}): CoreCancellablePromise { + const options = typeof optionsOrInterval === 'number' ? { interval: optionsOrInterval } : optionsOrInterval; + if (condition()) { return CoreCancellablePromise.resolve(); } + const startTime = Date.now(); let intervalId: number | undefined; return new CoreCancellablePromise( async (resolve) => { intervalId = window.setInterval(() => { - if (!condition()) { + if (!condition() && (!options.timeout || (Date.now() - startTime < options.timeout))) { return; } resolve(); window.clearInterval(intervalId); - }, interval); + }, options.interval ?? 50); }, () => window.clearInterval(intervalId), ); @@ -1939,6 +1945,14 @@ export type CoreUtilsOpenInAppOptions = InAppBrowserOptions & { originalUrl?: string; // Original URL to open (in case the URL was treated, e.g. to add a token or an auto-login). }; +/** + * Options for waiting. + */ +export type CoreUtilsWaitOptions = { + interval?: number; + timeout?: number; +}; + /** * Possible default picker actions. */ From c3817a0a60790548f11f7821cc34c0d1baccbaa5 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 8 Feb 2024 10:55:50 +0100 Subject: [PATCH 17/19] MOBILE-4304 scorm: Comment incompatible tests --- .../tests/behat/appearance_options.feature | 11 +- .../tests/behat/attempts_and_grading.feature | 1803 +++++++++-------- .../mod/scorm/tests/behat/basic_usage.feature | 195 +- src/testing/services/behat-runtime.ts | 4 + 4 files changed, 1013 insertions(+), 1000 deletions(-) diff --git a/src/addons/mod/scorm/tests/behat/appearance_options.feature b/src/addons/mod/scorm/tests/behat/appearance_options.feature index e4035d1189a..20f936ec5ad 100755 --- a/src/addons/mod/scorm/tests/behat/appearance_options.feature +++ b/src/addons/mod/scorm/tests/behat/appearance_options.feature @@ -4,6 +4,9 @@ Feature: Test appearance options of SCORM activity in app As a student I need appearance options to be applied properly + # SCORM iframes no longer work in the browser, hence the commented lines in this file. + # This should be reverted once MOBILE-4503 is solved. + Background: Given the following "users" exist: | username | firstname | lastname | email | @@ -28,14 +31,14 @@ Feature: Test appearance options of SCORM activity in app When I press "Current window SCORM" in the app And I press "Enter" in the app And I press "Disable fullscreen" in the app - Then the UI should match the snapshot + # Then the UI should match the snapshot When I press the back button in the app And I press the back button in the app And I press "New window px SCORM" in the app And I press "Enter" in the app And I press "Disable fullscreen" in the app - Then the UI should match the snapshot + # Then the UI should match the snapshot # SCORMs with percentage sizes are displayed with full size in the app. See MOBILE-3426 for details. When I press the back button in the app @@ -43,7 +46,7 @@ Feature: Test appearance options of SCORM activity in app And I press "New window perc SCORM" in the app And I press "Enter" in the app And I press "Disable fullscreen" in the app - Then the UI should match the snapshot + # Then the UI should match the snapshot Scenario: Skip SCORM entry page if needed Given the following "activities" exist: @@ -76,7 +79,7 @@ Feature: Test appearance options of SCORM activity in app And I press the back button in the app And I press "Always skip SCORM" in the app And I press "Disable fullscreen" in the app - Then I should find "3 / 11" in the app + # Then I should find "3 / 11" in the app Scenario: Disable preview mode Given the following "activities" exist: diff --git a/src/addons/mod/scorm/tests/behat/attempts_and_grading.feature b/src/addons/mod/scorm/tests/behat/attempts_and_grading.feature index 8545edd57cc..ecca4cd0784 100755 --- a/src/addons/mod/scorm/tests/behat/attempts_and_grading.feature +++ b/src/addons/mod/scorm/tests/behat/attempts_and_grading.feature @@ -4,6 +4,9 @@ Feature: Test attempts and grading settings of SCORM activity in app As a student I need attempts and grading settings to be applied properly + # SCORM iframes no longer work in the browser, hence the commented lines in this file. + # This should be reverted once MOBILE-4503 is solved. + Background: Given the following "users" exist: | username | firstname | lastname | email | @@ -68,7 +71,7 @@ Feature: Test attempts and grading settings of SCORM activity in app When I press "Enter" in the app And I press "Disable fullscreen" in the app And I press "TOC" in the app - Then I should find "Review mode" in the app + # Then I should find "Review mode" in the app When I press "Close" in the app And I press the back button in the app @@ -90,25 +93,25 @@ Feature: Test attempts and grading settings of SCORM activity in app Then I should find "1" within "Number of attempts you have made" "ion-item" in the app And I should not find "You have reached the maximum number of attempts." in the app - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press the back button in the app - Then I should find "2" within "Number of attempts you have made" "ion-item" in the app - And I should find "You have reached the maximum number of attempts." in the app - And I should not find "Start a new attempt" in the app - - When I press the back button in the app - And I press "SCORM unlimited" in the app - Then I should find "Unlimited" within "Number of attempts allowed" "ion-item" in the app + # When I press "Start a new attempt" in the app + # And I press "Enter" in the app + # And I press "Disable fullscreen" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press the back button in the app + # Then I should find "2" within "Number of attempts you have made" "ion-item" in the app + # And I should find "You have reached the maximum number of attempts." in the app + # And I should not find "Start a new attempt" in the app + + # When I press the back button in the app + # And I press "SCORM unlimited" in the app + # Then I should find "Unlimited" within "Number of attempts allowed" "ion-item" in the app Scenario: New attempts are started when they should based on 'Force new attempt' setting Given the following "activities" exist: @@ -130,892 +133,892 @@ Feature: Test attempts and grading settings of SCORM activity in app And I press "Next" in the app And I press the back button in the app Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - And I should find "Start a new attempt" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I press "TOC" in the app - Then I should find "Review mode" in the app - - When I press "Close" in the app - And I press the back button in the app - Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I press "TOC" in the app - Then I should not find "Review mode" in the app - - When I press "Close" in the app - And I press the back button in the app - Then I should find "2" within "Number of attempts you have made" "ion-item" in the app - And I should not find "Start a new attempt" in the app - - When I press the back button in the app - When I press "SCORM when completed" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press "Next" in the app - And I press the back button in the app - Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - And I should not find "Start a new attempt" in the app + # And I should find "Start a new attempt" in the app When I press "Enter" in the app And I press "Disable fullscreen" in the app And I press "TOC" in the app - Then I should not find "Review mode" in the app + # Then I should find "Review mode" in the app When I press "Close" in the app And I press the back button in the app - Then I should find "2" within "Number of attempts you have made" "ion-item" in the app - And I should not find "Start a new attempt" in the app - - When I press the back button in the app - When I press "SCORM always force" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I press "Next" in the app - And I press the back button in the app Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - And I should not find "Start a new attempt" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I press "Next" in the app - And I press the back button in the app - Then I should find "2" within "Number of attempts you have made" "ion-item" in the app - And I should not find "Start a new attempt" in the app - - Scenario: Attempt grade is calculated right based on 'Grading method' setting - Given the following "activities" exist: - | activity | name | course | idnumber | packagefilepath | maxattempt | grademethod | maxgrade | displaycoursestructure | - | scorm | SCORM scos | C1 | scorm | mod/scorm/tests/packages/complexscorm.zip | 0 | 0 | 100 | 1 | - | scorm | SCORM highest | C1 | scorm2 | mod/scorm/tests/packages/complexscorm.zip | 0 | 1 | 100 | 1 | - | scorm | SCORM average | C1 | scorm3 | mod/scorm/tests/packages/complexscorm.zip | 0 | 2 | 100 | 1 | - | scorm | SCORM sum 100 | C1 | scorm4 | mod/scorm/tests/packages/complexscorm.zip | 0 | 3 | 100 | 1 | - | scorm | SCORM sum 50 | C1 | scorm5 | mod/scorm/tests/packages/complexscorm.zip | 0 | 3 | 50 | 1 | - And I entered the course "Course 1" as "student1" in the app - - # Case 1: SCORM with learning objects as grading method - When I press "SCORM scos" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-26" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - And I should find "Passed" within "The first content (one SCO)" "ion-item" in the app - And I should find "Not attempted" within "The second content (one SCO too)" "ion-item" in the app - - When I press "The second content (one SCO too)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-28" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - And I should find "Passed" within "The first content (one SCO)" "ion-item" in the app - And I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app - And I should find "Completed" within "The second content (one SCO too)" "ion-item" in the app - - When I press "Third content (this is an asset)" in the app - And I press "TOC" in the app - And I press "SCO with subscoes" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-20" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app - And I should find "Failed" within "SCO with subscoes" "ion-item" in the app - - When I press "Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-22" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 4" in the app - - When I press "Sub-Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-24" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app - And I should find "Passed" within "Sub-Sub-SCO" "ion-item" in the app - - When I press "SCO with prerequisite (first and secon SCO)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-25" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 7" within "SCO with prerequisite (first and secon SCO)" "ion-item" in the app - And I should find "Completed" within "SCO with prerequisite (first and secon SCO)" "ion-item" in the app - - When I press "Close" in the app - And I press the back button in the app - Then I should find "5" within "Grade reported" "ion-item" in the app - And I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - # Case 2: SCORM with highest grade as grading method - When I press the back button in the app - And I press "SCORM highest" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-26" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - - When I press "The second content (one SCO too)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-28" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app - - When I press "Third content (this is an asset)" in the app - And I press "TOC" in the app - And I press "SCO with subscoes" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-20" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app - - When I press "Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-22" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 4" in the app - - When I press "Sub-Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-24" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app - - When I press "SCO with prerequisite (first and secon SCO)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-25" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press the back button in the app - Then I should find "10" within "Grade reported" "ion-item" in the app - - # Case 3: SCORM with average grade as grading method - When I press the back button in the app - And I press "SCORM average" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-26" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - - When I press "The second content (one SCO too)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-28" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app - - When I press "Third content (this is an asset)" in the app - And I press "TOC" in the app - And I press "SCO with subscoes" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-20" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app - - When I press "Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-22" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 4" in the app - - When I press "Sub-Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-24" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app - - When I press "SCO with prerequisite (first and secon SCO)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-25" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press the back button in the app - Then I should find "6.17%" within "Grade reported" "ion-item" in the app - - # Case 4: SCORM with sum grade as grading method and a max grade of 100 - When I press the back button in the app - And I press "SCORM sum 100" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-26" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - - When I press "The second content (one SCO too)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-28" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app - - When I press "Third content (this is an asset)" in the app - And I press "TOC" in the app - And I press "SCO with subscoes" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-20" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app - - When I press "Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-22" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 4" in the app - - When I press "Sub-Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-24" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app - - When I press "SCO with prerequisite (first and secon SCO)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-25" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press the back button in the app - Then I should find "37%" within "Grade reported" "ion-item" in the app - - # Case 5: SCORM with sum grade as grading method and a max grade of 50 - When I press the back button in the app - And I press "SCORM sum 50" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-26" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app - - When I press "The second content (one SCO too)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-28" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app - - When I press "Third content (this is an asset)" in the app - And I press "TOC" in the app - And I press "SCO with subscoes" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-20" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app - - When I press "Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-14" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-22" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 4" in the app - - When I press "Sub-Sub-SCO" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-24" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app - - When I press "SCO with prerequisite (first and secon SCO)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-25" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press the back button in the app - Then I should find "74%" within "Grade reported" "ion-item" in the app - - @lms_from4.1 - Scenario: SCORM grade is calculated right based on 'Attempts grading' setting - Given the following "activities" exist: - | activity | name | course | idnumber | packagefilepath | maxattempt | whatgrade | grademethod | forcenewattempt | - | scorm | SCORM highest | C1 | scorm | mod/scorm/tests/packages/singlescobasic.zip | 0 | 0 | 1 | 0 | - | scorm | SCORM average | C1 | scorm2 | mod/scorm/tests/packages/singlescobasic.zip | 0 | 1 | 1 | 0 | - | scorm | SCORM first | C1 | scorm3 | mod/scorm/tests/packages/singlescobasic.zip | 0 | 2 | 1 | 0 | - | scorm | SCORM last | C1 | scorm4 | mod/scorm/tests/packages/singlescobasic.zip | 0 | 3 | 1 | 0 | - And I entered the course "Course 1" as "student1" in the app - - # Case 1: perform 3 attempts in 'SCORM highest' and check the highest grade is the one used. - When I press "SCORM highest" in the app - Then I should find "Highest attempt" within "Grading method" "ion-item" in the app - And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I press "Submit Answers" - Then I should see "Score: 27" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - And I press "Grades" in the app - Then I should find "27%" within "Grade reported" "ion-item" in the app - And I should find "27%" within "Grade for attempt 1" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" - And I press "Submit Answers" - Then I should see "Score: 40" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - Then I should find "40%" within "Grade reported" "ion-item" in the app - And I should find "27%" within "Grade for attempt 1" "ion-item" in the app - And I should find "40%" within "Grade for attempt 2" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I press "Submit Answers" - Then I should see "Score: 20" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - Then I should find "40%" within "Grade reported" "ion-item" in the app - And I should find "27%" within "Grade for attempt 1" "ion-item" in the app - And I should find "40%" within "Grade for attempt 2" "ion-item" in the app - And I should find "20%" within "Grade for attempt 3" "ion-item" in the app - - # Case 2: perform 2 attempts in 'SCORM average' and check the average grade is used. - When I press the back button in the app - And I press "SCORM average" in the app - Then I should find "Average attempts" within "Grading method" "ion-item" in the app - And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I press "Submit Answers" - Then I should see "Score: 20" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - And I press "Grades" in the app - Then I should find "20%" within "Grade reported" "ion-item" in the app - And I should find "20%" within "Grade for attempt 1" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" - And I press "Submit Answers" - Then I should see "Score: 40" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - Then I should find "30%" within "Grade reported" "ion-item" in the app - And I should find "20%" within "Grade for attempt 1" "ion-item" in the app - And I should find "40%" within "Grade for attempt 2" "ion-item" in the app - - # Case 3: perform 2 attempts in 'SCORM first' and check the first attempt is used. - When I press the back button in the app - And I press "SCORM first" in the app - Then I should find "First attempt" within "Grading method" "ion-item" in the app - And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I press "Submit Answers" - Then I should see "Score: 27" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - And I press "Grades" in the app - Then I should find "27%" within "Grade reported" "ion-item" in the app - And I should find "27%" within "Grade for attempt 1" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" - And I press "Submit Answers" - Then I should see "Score: 40" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - Then I should find "27%" within "Grade reported" "ion-item" in the app - And I should find "27%" within "Grade for attempt 1" "ion-item" in the app - And I should find "40%" within "Grade for attempt 2" "ion-item" in the app - - # Case 4: perform 3 attempts in 'SCORM last' and check the last completed attempt is used. - When I press the back button in the app - And I press "SCORM last" in the app - Then I should find "Last completed attempt" within "Grading method" "ion-item" in the app - And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I set the field with xpath "//input[@id='question_com.scorm.golfsamples.interactions.playing_3_Text']" to "18" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" - And I set the field with xpath "//input[@id='question_com.scorm.golfsamples.interactions.playing_5_Text']" to "3" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_2_True" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_3_0" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.handicap_1_2" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_1_False" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_2_False" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_3_False" "css_element" - And I press "Submit Answers" - Then I should see "Score: 87" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - And I press "Grades" in the app - Then I should find "87%" within "Grade reported" "ion-item" in the app - And I should find "87%" within "Grade for attempt 1" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I press "Submit Answers" - Then I should see "Score: 27" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - # Grade reported belongs to attempt 1 because the second attempt's only SCO is failed. - Then I should find "87%" within "Grade reported" "ion-item" in the app - And I should find "87%" within "Grade for attempt 1" "ion-item" in the app - And I should find "27%" within "Grade for attempt 2" "ion-item" in the app - - When I press "Start a new attempt" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - And I switch to "scorm_object" iframe - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I press "Next" - And I switch to "contentFrame" iframe - Then I should see "Knowledge Check" - - When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_2_True" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_3_0" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.handicap_1_2" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_1_False" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_2_False" "css_element" - And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_3_False" "css_element" - And I press "Submit Answers" - Then I should see "Score: 73" - - When I switch to the main frame - And I switch to "scorm_object" iframe - And I press "Exit" - And I switch to the main frame - And I press the back button in the app - Then I should find "73%" within "Grade reported" "ion-item" in the app - And I should find "87%" within "Grade for attempt 1" "ion-item" in the app - And I should find "27%" within "Grade for attempt 2" "ion-item" in the app - And I should find "73%" within "Grade for attempt 3" "ion-item" in the app + # When I press "Start a new attempt" in the app + # And I press "Enter" in the app + # And I press "Disable fullscreen" in the app + # And I press "TOC" in the app + # Then I should not find "Review mode" in the app + + # When I press "Close" in the app + # And I press the back button in the app + # Then I should find "2" within "Number of attempts you have made" "ion-item" in the app + # And I should not find "Start a new attempt" in the app + + # When I press the back button in the app + # When I press "SCORM when completed" in the app + # And I press "Enter" in the app + # And I press "Disable fullscreen" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press "Next" in the app + # And I press the back button in the app + # Then I should find "1" within "Number of attempts you have made" "ion-item" in the app + # And I should not find "Start a new attempt" in the app + + # When I press "Enter" in the app + # And I press "Disable fullscreen" in the app + # And I press "TOC" in the app + # Then I should not find "Review mode" in the app + + # When I press "Close" in the app + # And I press the back button in the app + # Then I should find "2" within "Number of attempts you have made" "ion-item" in the app + # And I should not find "Start a new attempt" in the app + + # When I press the back button in the app + # When I press "SCORM always force" in the app + # And I press "Enter" in the app + # And I press "Disable fullscreen" in the app + # And I press "Next" in the app + # And I press the back button in the app + # Then I should find "1" within "Number of attempts you have made" "ion-item" in the app + # And I should not find "Start a new attempt" in the app + + # When I press "Enter" in the app + # And I press "Disable fullscreen" in the app + # And I press "Next" in the app + # And I press the back button in the app + # Then I should find "2" within "Number of attempts you have made" "ion-item" in the app + # And I should not find "Start a new attempt" in the app + +# Scenario: Attempt grade is calculated right based on 'Grading method' setting +# Given the following "activities" exist: +# | activity | name | course | idnumber | packagefilepath | maxattempt | grademethod | maxgrade | displaycoursestructure | +# | scorm | SCORM scos | C1 | scorm | mod/scorm/tests/packages/complexscorm.zip | 0 | 0 | 100 | 1 | +# | scorm | SCORM highest | C1 | scorm2 | mod/scorm/tests/packages/complexscorm.zip | 0 | 1 | 100 | 1 | +# | scorm | SCORM average | C1 | scorm3 | mod/scorm/tests/packages/complexscorm.zip | 0 | 2 | 100 | 1 | +# | scorm | SCORM sum 100 | C1 | scorm4 | mod/scorm/tests/packages/complexscorm.zip | 0 | 3 | 100 | 1 | +# | scorm | SCORM sum 50 | C1 | scorm5 | mod/scorm/tests/packages/complexscorm.zip | 0 | 3 | 50 | 1 | +# And I entered the course "Course 1" as "student1" in the app + +# # Case 1: SCORM with learning objects as grading method +# When I press "SCORM scos" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-26" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app +# And I should find "Passed" within "The first content (one SCO)" "ion-item" in the app +# And I should find "Not attempted" within "The second content (one SCO too)" "ion-item" in the app + +# When I press "The second content (one SCO too)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-28" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app +# And I should find "Passed" within "The first content (one SCO)" "ion-item" in the app +# And I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app +# And I should find "Completed" within "The second content (one SCO too)" "ion-item" in the app + +# When I press "Third content (this is an asset)" in the app +# And I press "TOC" in the app +# And I press "SCO with subscoes" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-20" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app +# And I should find "Failed" within "SCO with subscoes" "ion-item" in the app + +# When I press "Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-22" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 4" in the app + +# When I press "Sub-Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-24" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app +# And I should find "Passed" within "Sub-Sub-SCO" "ion-item" in the app + +# When I press "SCO with prerequisite (first and secon SCO)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-25" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 7" within "SCO with prerequisite (first and secon SCO)" "ion-item" in the app +# And I should find "Completed" within "SCO with prerequisite (first and secon SCO)" "ion-item" in the app + +# When I press "Close" in the app +# And I press the back button in the app +# Then I should find "5" within "Grade reported" "ion-item" in the app +# And I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app + +# # Case 2: SCORM with highest grade as grading method +# When I press the back button in the app +# And I press "SCORM highest" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-26" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app + +# When I press "The second content (one SCO too)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-28" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app + +# When I press "Third content (this is an asset)" in the app +# And I press "TOC" in the app +# And I press "SCO with subscoes" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-20" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app + +# When I press "Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-22" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 4" in the app + +# When I press "Sub-Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-24" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app + +# When I press "SCO with prerequisite (first and secon SCO)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-25" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "10" within "Grade reported" "ion-item" in the app + +# # Case 3: SCORM with average grade as grading method +# When I press the back button in the app +# And I press "SCORM average" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-26" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app + +# When I press "The second content (one SCO too)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-28" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app + +# When I press "Third content (this is an asset)" in the app +# And I press "TOC" in the app +# And I press "SCO with subscoes" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-20" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app + +# When I press "Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-22" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 4" in the app + +# When I press "Sub-Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-24" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app + +# When I press "SCO with prerequisite (first and secon SCO)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-25" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "6.17%" within "Grade reported" "ion-item" in the app + +# # Case 4: SCORM with sum grade as grading method and a max grade of 100 +# When I press the back button in the app +# And I press "SCORM sum 100" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-26" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app + +# When I press "The second content (one SCO too)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-28" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app + +# When I press "Third content (this is an asset)" in the app +# And I press "TOC" in the app +# And I press "SCO with subscoes" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-20" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app + +# When I press "Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-22" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 4" in the app + +# When I press "Sub-Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-24" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app + +# When I press "SCO with prerequisite (first and secon SCO)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-25" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "37%" within "Grade reported" "ion-item" in the app + +# # Case 5: SCORM with sum grade as grading method and a max grade of 50 +# When I press the back button in the app +# And I press "SCORM sum 50" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-26" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 8" within "The first content (one SCO)" "ion-item" in the app + +# When I press "The second content (one SCO too)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-28" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 10" within "The second content (one SCO too)" "ion-item" in the app + +# When I press "Third content (this is an asset)" in the app +# And I press "TOC" in the app +# And I press "SCO with subscoes" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-20" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 2" within "SCO with subscoes" "ion-item" in the app + +# When I press "Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-14" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-22" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 4" in the app + +# When I press "Sub-Sub-SCO" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-12" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-24" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press "TOC" in the app +# Then I should find "Score: 6" within "Sub-Sub-SCO" "ion-item" in the app + +# When I press "SCO with prerequisite (first and secon SCO)" in the app +# And I switch to "scorm_object" iframe +# And I click on "Common operations" "link" +# And I click on "#set-lesson-status-button" "css_element" +# And I click on "#ui-id-13" "css_element" +# And I click on "#set-score-button" "css_element" +# And I click on "#ui-id-25" "css_element" +# And I press "Commit changes" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "74%" within "Grade reported" "ion-item" in the app + +# @lms_from4.1 +# Scenario: SCORM grade is calculated right based on 'Attempts grading' setting +# Given the following "activities" exist: +# | activity | name | course | idnumber | packagefilepath | maxattempt | whatgrade | grademethod | forcenewattempt | +# | scorm | SCORM highest | C1 | scorm | mod/scorm/tests/packages/singlescobasic.zip | 0 | 0 | 1 | 0 | +# | scorm | SCORM average | C1 | scorm2 | mod/scorm/tests/packages/singlescobasic.zip | 0 | 1 | 1 | 0 | +# | scorm | SCORM first | C1 | scorm3 | mod/scorm/tests/packages/singlescobasic.zip | 0 | 2 | 1 | 0 | +# | scorm | SCORM last | C1 | scorm4 | mod/scorm/tests/packages/singlescobasic.zip | 0 | 3 | 1 | 0 | +# And I entered the course "Course 1" as "student1" in the app + +# # Case 1: perform 3 attempts in 'SCORM highest' and check the highest grade is the one used. +# When I press "SCORM highest" in the app +# Then I should find "Highest attempt" within "Grading method" "ion-item" in the app +# And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app + +# When I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 27" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# And I press "Grades" in the app +# Then I should find "27%" within "Grade reported" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 1" "ion-item" in the app + +# When I press "Start a new attempt" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 40" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "40%" within "Grade reported" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 1" "ion-item" in the app +# And I should find "40%" within "Grade for attempt 2" "ion-item" in the app + +# When I press "Start a new attempt" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 20" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "40%" within "Grade reported" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 1" "ion-item" in the app +# And I should find "40%" within "Grade for attempt 2" "ion-item" in the app +# And I should find "20%" within "Grade for attempt 3" "ion-item" in the app + +# # Case 2: perform 2 attempts in 'SCORM average' and check the average grade is used. +# When I press the back button in the app +# And I press "SCORM average" in the app +# Then I should find "Average attempts" within "Grading method" "ion-item" in the app +# And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app + +# When I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 20" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# And I press "Grades" in the app +# Then I should find "20%" within "Grade reported" "ion-item" in the app +# And I should find "20%" within "Grade for attempt 1" "ion-item" in the app + +# When I press "Start a new attempt" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 40" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "30%" within "Grade reported" "ion-item" in the app +# And I should find "20%" within "Grade for attempt 1" "ion-item" in the app +# And I should find "40%" within "Grade for attempt 2" "ion-item" in the app + +# # Case 3: perform 2 attempts in 'SCORM first' and check the first attempt is used. +# When I press the back button in the app +# And I press "SCORM first" in the app +# Then I should find "First attempt" within "Grading method" "ion-item" in the app +# And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app + +# When I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 27" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# And I press "Grades" in the app +# Then I should find "27%" within "Grade reported" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 1" "ion-item" in the app + +# When I press "Start a new attempt" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 40" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "27%" within "Grade reported" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 1" "ion-item" in the app +# And I should find "40%" within "Grade for attempt 2" "ion-item" in the app + +# # Case 4: perform 3 attempts in 'SCORM last' and check the last completed attempt is used. +# When I press the back button in the app +# And I press "SCORM last" in the app +# Then I should find "Last completed attempt" within "Grading method" "ion-item" in the app +# And I should find "Grade couldn't be calculated" within "Grade reported" "ion-item" in the app + +# When I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I set the field with xpath "//input[@id='question_com.scorm.golfsamples.interactions.playing_3_Text']" to "18" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" +# And I set the field with xpath "//input[@id='question_com.scorm.golfsamples.interactions.playing_5_Text']" to "3" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_2_True" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_3_0" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.handicap_1_2" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_1_False" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_2_False" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_3_False" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 87" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# And I press "Grades" in the app +# Then I should find "87%" within "Grade reported" "ion-item" in the app +# And I should find "87%" within "Grade for attempt 1" "ion-item" in the app + +# When I press "Start a new attempt" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 27" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# # Grade reported belongs to attempt 1 because the second attempt's only SCO is failed. +# Then I should find "87%" within "Grade reported" "ion-item" in the app +# And I should find "87%" within "Grade for attempt 1" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 2" "ion-item" in the app + +# When I press "Start a new attempt" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# And I switch to "scorm_object" iframe +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I press "Next" +# And I switch to "contentFrame" iframe +# Then I should see "Knowledge Check" + +# When I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_1_1" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_2_3" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.playing_4_True" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_2_True" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_1_2" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.etiquette_3_0" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.handicap_1_2" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_1_False" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_2_False" "css_element" +# And I click on "#question_com\.scorm\.golfsamples\.interactions\.fun_3_False" "css_element" +# And I press "Submit Answers" +# Then I should see "Score: 73" + +# When I switch to the main frame +# And I switch to "scorm_object" iframe +# And I press "Exit" +# And I switch to the main frame +# And I press the back button in the app +# Then I should find "73%" within "Grade reported" "ion-item" in the app +# And I should find "87%" within "Grade for attempt 1" "ion-item" in the app +# And I should find "27%" within "Grade for attempt 2" "ion-item" in the app +# And I should find "73%" within "Grade for attempt 3" "ion-item" in the app diff --git a/src/addons/mod/scorm/tests/behat/basic_usage.feature b/src/addons/mod/scorm/tests/behat/basic_usage.feature index 14766cb3b1e..b3316c8efbc 100755 --- a/src/addons/mod/scorm/tests/behat/basic_usage.feature +++ b/src/addons/mod/scorm/tests/behat/basic_usage.feature @@ -4,6 +4,9 @@ Feature: Test basic usage of SCORM activity in app As a student I need basic SCORM functionality to work + # SCORM iframes no longer work in the browser, hence the commented lines in this file. + # This should be reverted once MOBILE-4503 is solved. + Background: Given the following "users" exist: | username | firstname | lastname | email | @@ -17,35 +20,35 @@ Feature: Test basic usage of SCORM activity in app | teacher1 | C1 | editingteacher | | student1 | C1 | student | - Scenario: Resume progress when re-entering SCORM - Given the following "activities" exist: - | activity | name | intro | course | idnumber | packagefilepath | - | scorm | Basic SCORM | SCORM description | C1 | scorm | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | - And I entered the course "Course 1" as "student1" in the app - When I press "Basic SCORM" in the app - And I press "Enter" in the app - And I press "Disable fullscreen" in the app - Then I should find "2 / 11" in the app - And I switch to "scorm_object" iframe - And I should see "Play of the game" - - When I switch to the main frame - And I press "Next" in the app - And I press "Next" in the app - Then I should find "4 / 11" in the app - And I switch to "scorm_object" iframe - And I should see "Scoring" - - When I switch to the main frame - And I press the back button in the app - Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - And I should find "3" within "Grade reported" "ion-item" in the app - - When I press "Enter" in the app - And I press "Disable fullscreen" in the app - Then I should find "5 / 11" in the app - And I switch to "scorm_object" iframe - And I should see "Other Scoring Systems" +# Scenario: Resume progress when re-entering SCORM +# Given the following "activities" exist: +# | activity | name | intro | course | idnumber | packagefilepath | +# | scorm | Basic SCORM | SCORM description | C1 | scorm | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | +# And I entered the course "Course 1" as "student1" in the app +# When I press "Basic SCORM" in the app +# And I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# Then I should find "2 / 11" in the app +# And I switch to "scorm_object" iframe +# And I should see "Play of the game" + +# When I switch to the main frame +# And I press "Next" in the app +# And I press "Next" in the app +# Then I should find "4 / 11" in the app +# And I switch to "scorm_object" iframe +# And I should see "Scoring" + +# When I switch to the main frame +# And I press the back button in the app +# Then I should find "1" within "Number of attempts you have made" "ion-item" in the app +# And I should find "3" within "Grade reported" "ion-item" in the app + +# When I press "Enter" in the app +# And I press "Disable fullscreen" in the app +# Then I should find "5 / 11" in the app +# And I switch to "scorm_object" iframe +# And I should see "Other Scoring Systems" Scenario: TOC displays the right status and opens the right SCO Given the following "activities" exist: @@ -79,42 +82,42 @@ Feature: Test basic usage of SCORM activity in app When I press "Close" in the app And I press "Next" in the app And I press "TOC" in the app - Then I should find "Completed" within "How to Play" "ion-item" in the app - And I should find "Not attempted" within "Par?" "ion-item" in the app + # Then I should find "Completed" within "How to Play" "ion-item" in the app + # And I should find "Not attempted" within "Par?" "ion-item" in the app When I press "The Rules of Golf" in the app Then I should find "6 / 11" in the app - And I switch to "scorm_object" iframe - And I should see "The Rules of Golf" - - When I switch to the main frame - And I press "TOC" in the app - Then I should find "Completed" within "How to Play" "ion-item" in the app - And I should find "Completed" within "Par?" "ion-item" in the app - And I should find "Not attempted" within "Keeping Score" "ion-item" in the app - And I should find "Not attempted" within "Other Scoring Systems" "ion-item" in the app - And I should find "Not attempted" within "The Rules of Golf" "ion-item" in the app - And I should find "Not attempted" within "Playing Golf Quiz" "ion-item" in the app - And I should find "Not attempted" within "How to Have Fun Playing Golf" "ion-item" in the app - And I should find "Not attempted" within "How to Make Friends Playing Golf" "ion-item" in the app - And I should find "Not attempted" within "Having Fun Quiz" "ion-item" in the app - - When I press "Close" in the app - And I press the back button in the app - Then I should find "Completed" within "How to Play" "ion-item" in the app - And I should find "Completed" within "Par?" "ion-item" in the app - And I should find "Not attempted" within "Keeping Score" "ion-item" in the app - And I should find "Not attempted" within "Other Scoring Systems" "ion-item" in the app - And I should find "Completed" within "The Rules of Golf" "ion-item" in the app - And I should find "Not attempted" within "Playing Golf Quiz" "ion-item" in the app - And I should find "Not attempted" within "How to Have Fun Playing Golf" "ion-item" in the app - And I should find "Not attempted" within "How to Make Friends Playing Golf" "ion-item" in the app - And I should find "Not attempted" within "Having Fun Quiz" "ion-item" in the app - - When I press "How to Have Fun Playing Golf" in the app - Then I should find "9 / 11" in the app - And I switch to "scorm_object" iframe - And I should see "How to Have Fun Golfing" + # And I switch to "scorm_object" iframe + # And I should see "The Rules of Golf" + + # When I switch to the main frame + # And I press "TOC" in the app + # Then I should find "Completed" within "How to Play" "ion-item" in the app + # And I should find "Completed" within "Par?" "ion-item" in the app + # And I should find "Not attempted" within "Keeping Score" "ion-item" in the app + # And I should find "Not attempted" within "Other Scoring Systems" "ion-item" in the app + # And I should find "Not attempted" within "The Rules of Golf" "ion-item" in the app + # And I should find "Not attempted" within "Playing Golf Quiz" "ion-item" in the app + # And I should find "Not attempted" within "How to Have Fun Playing Golf" "ion-item" in the app + # And I should find "Not attempted" within "How to Make Friends Playing Golf" "ion-item" in the app + # And I should find "Not attempted" within "Having Fun Quiz" "ion-item" in the app + + # When I press "Close" in the app + # And I press the back button in the app + # Then I should find "Completed" within "How to Play" "ion-item" in the app + # And I should find "Completed" within "Par?" "ion-item" in the app + # And I should find "Not attempted" within "Keeping Score" "ion-item" in the app + # And I should find "Not attempted" within "Other Scoring Systems" "ion-item" in the app + # And I should find "Completed" within "The Rules of Golf" "ion-item" in the app + # And I should find "Not attempted" within "Playing Golf Quiz" "ion-item" in the app + # And I should find "Not attempted" within "How to Have Fun Playing Golf" "ion-item" in the app + # And I should find "Not attempted" within "How to Make Friends Playing Golf" "ion-item" in the app + # And I should find "Not attempted" within "Having Fun Quiz" "ion-item" in the app + + # When I press "How to Have Fun Playing Golf" in the app + # Then I should find "9 / 11" in the app + # And I switch to "scorm_object" iframe + # And I should see "How to Have Fun Golfing" Scenario: Preview SCORM Given the following "activities" exist: @@ -142,19 +145,19 @@ Feature: Test basic usage of SCORM activity in app When I press the back button in the app Then I should find "1" within "Number of attempts you have made" "ion-item" in the app - And I should find "9" within "Grade reported" "ion-item" in the app + # And I should find "9" within "Grade reported" "ion-item" in the app - # Check that Preview doesn't start a new attempt. - When I press "Start a new attempt" in the app - And I press "Preview" in the app - And I press "Disable fullscreen" in the app - And I press "TOC" in the app - Then I should find "Complete" within "How to Play" "ion-item" in the app - And I should find "Complete" within "Having Fun Quiz" "ion-item" in the app + # # Check that Preview doesn't start a new attempt. + # When I press "Start a new attempt" in the app + # And I press "Preview" in the app + # And I press "Disable fullscreen" in the app + # And I press "TOC" in the app + # Then I should find "Complete" within "How to Play" "ion-item" in the app + # And I should find "Complete" within "Having Fun Quiz" "ion-item" in the app - When I press "Close" in the app - And I press the back button in the app - Then I should find "1" within "Number of attempts you have made" "ion-item" in the app + # When I press "Close" in the app + # And I press the back button in the app + # Then I should find "1" within "Number of attempts you have made" "ion-item" in the app Scenario: Unsupported SCORM Given the following "activities" exist: @@ -194,29 +197,29 @@ Feature: Test basic usage of SCORM activity in app When I press "The first content (one SCO)" in the app And I press "Disable fullscreen" in the app And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-12" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-26" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Passed" within "The first content (one SCO)" "ion-item" in the app - And I should not be able to press "SCO with prerequisite (first and secon SCO)" in the app - - When I press "The second content (one SCO too)" in the app - And I switch to "scorm_object" iframe - And I click on "Common operations" "link" - And I click on "#set-lesson-status-button" "css_element" - And I click on "#ui-id-13" "css_element" - And I click on "#set-score-button" "css_element" - And I click on "#ui-id-28" "css_element" - And I press "Commit changes" - And I switch to the main frame - And I press "TOC" in the app - Then I should find "Completed" within "The second content (one SCO too)" "ion-item" in the app - And I should be able to press "SCO with prerequisite (first and secon SCO)" in the app + # And I click on "Common operations" "link" + # And I click on "#set-lesson-status-button" "css_element" + # And I click on "#ui-id-12" "css_element" + # And I click on "#set-score-button" "css_element" + # And I click on "#ui-id-26" "css_element" + # And I press "Commit changes" + # And I switch to the main frame + # And I press "TOC" in the app + # Then I should find "Passed" within "The first content (one SCO)" "ion-item" in the app + # And I should not be able to press "SCO with prerequisite (first and secon SCO)" in the app + + # When I press "The second content (one SCO too)" in the app + # And I switch to "scorm_object" iframe + # And I click on "Common operations" "link" + # And I click on "#set-lesson-status-button" "css_element" + # And I click on "#ui-id-13" "css_element" + # And I click on "#set-score-button" "css_element" + # And I click on "#ui-id-28" "css_element" + # And I press "Commit changes" + # And I switch to the main frame + # And I press "TOC" in the app + # Then I should find "Completed" within "The second content (one SCO too)" "ion-item" in the app + # And I should be able to press "SCO with prerequisite (first and secon SCO)" in the app @lms_from4.2 Scenario: View events are stored in the log diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index ea9ad550d04..17078b0afb4 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -32,6 +32,7 @@ import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation'; import { Swiper } from 'swiper'; import { LocalNotificationsMock } from '@features/emulator/services/local-notifications'; import { GetClosureArgs } from '@/core/utils/types'; +import { CoreIframeComponent } from '@components/iframe/iframe'; /** * Behat runtime servive with public API. @@ -103,6 +104,9 @@ export class TestingBehatRuntimeService { return originalOpen(...args); }; + + // Reduce iframes timeout to speed up tests. + CoreIframeComponent.loadingTimeout = 1000; } /** From c80674776a3c535305a30b4d8ebe70c4773d70ce Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 8 Feb 2024 15:29:52 +0100 Subject: [PATCH 18/19] MOBILE-4304 ci: Configure test browser --- .github/workflows/acceptance.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index d30a6611f5b..aa88e95578e 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -189,11 +189,13 @@ jobs: - name: Initialise moodle-plugin-ci run: | - composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4.3 + git clone https://github.com/NoelDeMartin/moodle-plugin-ci --branch selenium-env ci + composer install -d ./ci echo $(cd ci/bin; pwd) >> $GITHUB_PATH echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH sudo locale-gen en_AU.UTF-8 echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV + sed -i "58i\$CFG->behat_profiles['chrome']['capabilities'] = ['extra_capabilities' => ['chromeOptions' => ['args' => ['--ignore-certificate-errors', '--allow-running-insecure-content']]]];" ci/res/template/config.php.txt - name: Install Behat Snapshots plugin run: moodle-plugin-ci add-plugin NoelDeMartin/moodle-local_behatsnapshots @@ -214,6 +216,7 @@ jobs: run: moodle-plugin-ci behat --auto-rerun 3 --profile chrome --tags="@app&&~@local&&$BEHAT_TAGS" env: BEHAT_TAGS: ${{ matrix.tags }} + MOODLE_BEHAT_SELENIUM_IMAGE: selenium/standalone-chrome:120.0 - name: Upload Snapshot failures uses: actions/upload-artifact@v4 From d94402637e5d4dc187f8203f8295c6a808b2c383 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 12 Feb 2024 15:13:22 +0100 Subject: [PATCH 19/19] MOBILE-4304 core: Implement SubPartial helper --- src/core/classes/database/database-table-proxy.ts | 5 +++-- src/core/classes/database/database-table.ts | 5 +++-- src/core/classes/database/debug-database-table.ts | 3 ++- src/core/classes/database/eager-database-table.ts | 3 ++- src/core/classes/database/inmemory-database-table.ts | 3 ++- src/core/classes/database/lazy-database-table.ts | 3 ++- src/core/features/h5p/classes/framework.ts | 5 +++-- src/core/features/login/components/site-help/site-help.ts | 3 ++- src/core/utils/types.ts | 5 +++++ 9 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index 7fd63308c7e..2aca136a904 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -29,6 +29,7 @@ import { import { CoreDebugDatabaseTable } from './debug-database-table'; import { CoreEagerDatabaseTable } from './eager-database-table'; import { CoreLazyDatabaseTable } from './lazy-database-table'; +import { SubPartial } from '@/core/utils/types'; /** * Database table proxy used to route database interactions through different implementations. @@ -155,14 +156,14 @@ export class CoreDatabaseTableProxy< /** * @inheritdoc */ - async insert(record: Omit & Partial>): Promise { + async insert(record: SubPartial): Promise { return this.target.insert(record); } /** * @inheritdoc */ - syncInsert(record: Omit & Partial>): void { + syncInsert(record: SubPartial): void { this.target.syncInsert(record); } diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index c52cb7352e6..856eaa892d9 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { SubPartial } from '@/core/utils/types'; import { CoreError } from '@classes/errors/error'; import { SQLiteDB, SQLiteDBRecordValue, SQLiteDBRecordValues } from '@classes/sqlitedb'; @@ -259,7 +260,7 @@ export class CoreDatabaseTable< * @param record Database record. * @returns New record row id. */ - async insert(record: Omit & Partial>): Promise { + async insert(record: SubPartial): Promise { const rowId = await this.database.insertRecord(this.tableName, record); return rowId; @@ -270,7 +271,7 @@ export class CoreDatabaseTable< * * @param record Database record. */ - syncInsert(record: Omit & Partial>): void { + syncInsert(record: SubPartial): void { // The current database architecture does not support synchronous operations, // so calling this method will mean that errors will be silenced. Because of that, // this should only be called if using the asynchronous alternatives is not possible. diff --git a/src/core/classes/database/debug-database-table.ts b/src/core/classes/database/debug-database-table.ts index cdef3494700..a1cd08865b5 100644 --- a/src/core/classes/database/debug-database-table.ts +++ b/src/core/classes/database/debug-database-table.ts @@ -21,6 +21,7 @@ import { CoreDatabaseReducer, CoreDatabaseQueryOptions, } from './database-table'; +import { SubPartial } from '@/core/utils/types'; /** * Database table proxy used to debug runtime operations. @@ -153,7 +154,7 @@ export class CoreDebugDatabaseTable< /** * @inheritdoc */ - insert(record: Omit & Partial>): Promise { + insert(record: SubPartial): Promise { this.logger.log('insert', record); return this.target.insert(record); diff --git a/src/core/classes/database/eager-database-table.ts b/src/core/classes/database/eager-database-table.ts index c5c75f9c986..3bfbf6e8dc4 100644 --- a/src/core/classes/database/eager-database-table.ts +++ b/src/core/classes/database/eager-database-table.ts @@ -21,6 +21,7 @@ import { CoreDatabaseReducer, CoreDatabaseQueryOptions, } from './database-table'; +import { SubPartial } from '@/core/utils/types'; /** * Wrapper used to improve performance by caching all the records for faster read operations. @@ -154,7 +155,7 @@ export class CoreEagerDatabaseTable< /** * @inheritdoc */ - async insert(record: Omit & Partial>): Promise { + async insert(record: SubPartial): Promise { const rowId = await this.insertAndRemember(record, this.records); return rowId; diff --git a/src/core/classes/database/inmemory-database-table.ts b/src/core/classes/database/inmemory-database-table.ts index 3af78a4708e..7f01ad912a4 100644 --- a/src/core/classes/database/inmemory-database-table.ts +++ b/src/core/classes/database/inmemory-database-table.ts @@ -16,6 +16,7 @@ import { CoreConstants } from '@/core/constants'; import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; import { CoreLogger } from '@singletons/logger'; import { CoreDatabaseTable, GetDBRecordPrimaryKey } from './database-table'; +import { SubPartial } from '@/core/utils/types'; /** * Database wrapper that caches database records in memory to speed up read operations. @@ -79,7 +80,7 @@ export abstract class CoreInMemoryDatabaseTable< * @returns New record row id. */ protected async insertAndRemember( - record: Omit & Partial>, + record: SubPartial, records: Record, ): Promise { const rowId = await super.insert(record); diff --git a/src/core/classes/database/lazy-database-table.ts b/src/core/classes/database/lazy-database-table.ts index c0e049e43b3..a745e29f4e4 100644 --- a/src/core/classes/database/lazy-database-table.ts +++ b/src/core/classes/database/lazy-database-table.ts @@ -21,6 +21,7 @@ import { GetDBRecordPrimaryKey, CoreDatabaseQueryOptions, } from './database-table'; +import { SubPartial } from '@/core/utils/types'; /** * Wrapper used to improve performance by caching records that are used often for faster read operations. @@ -138,7 +139,7 @@ export class CoreLazyDatabaseTable< /** * @inheritdoc */ - async insert(record: Omit & Partial>): Promise { + async insert(record: SubPartial): Promise { const rowId = await this.insertAndRemember(record, this.records); return rowId; diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index 6689fa20fa5..0e488f4a7f7 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -47,6 +47,7 @@ import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; import { LazyMap, lazyMap } from '@/core/utils/lazy-map'; import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; +import { SubPartial } from '@/core/utils/types'; /** * Equivalent to Moodle's implementation of H5PFrameworkInterface. @@ -780,7 +781,7 @@ export class CoreH5PFramework { embedTypes = libraryData.embedTypes.join(', '); } - const data: Omit & Partial> = { + const data: SubPartial = { title: libraryData.title, machinename: libraryData.machineName, majorversion: libraryData.majorVersion, @@ -930,7 +931,7 @@ export class CoreH5PFramework { throw new CoreError('Attempted to create content of library without id'); } - const data: Omit & Partial> = { + const data: SubPartial = { jsoncontent: content.params ?? '{}', mainlibraryid: content.library?.libraryId, timemodified: Date.now(), diff --git a/src/core/features/login/components/site-help/site-help.ts b/src/core/features/login/components/site-help/site-help.ts index ba1b6101dce..2bb6fa9239c 100644 --- a/src/core/features/login/components/site-help/site-help.ts +++ b/src/core/features/login/components/site-help/site-help.ts @@ -19,6 +19,7 @@ import { ModalController, Translate } from '@singletons'; import { FAQ_QRCODE_IMAGE_HTML, FAQ_URL_IMAGE_HTML, GET_STARTED_URL } from '@features/login/constants'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { SubPartial } from '@/core/utils/types'; /** * Component that displays help to connect to a site. @@ -217,5 +218,5 @@ enum AnswerFormat { * Question definition. */ type QuestionDefinition = Omit & { - answer: Omit & Partial>; + answer: SubPartial; }; diff --git a/src/core/utils/types.ts b/src/core/utils/types.ts index 30a311d82e6..bb4b85917f9 100644 --- a/src/core/utils/types.ts +++ b/src/core/utils/types.ts @@ -34,6 +34,11 @@ export type GetClosureArgs = T extends (...args: infer TArgs) => any ? TArgs */ export type Pretty = T extends infer U ? {[K in keyof U]: U[K]} : never; +/** + * Helper to convert some keys of an object to optional. + */ +export type SubPartial = Omit & Partial>; + /** * Helper type to omit union. * You can use it if need to omit an element from types union.