mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-10 03:32:52 +00:00
feat: improved e2e tests
This commit is contained in:
@@ -1,43 +1,86 @@
|
||||
// ***********************************************
|
||||
// This example namespace declaration will help
|
||||
// with Intellisense and code completion in your
|
||||
// IDE or Text Editor.
|
||||
// ***********************************************
|
||||
// declare namespace Cypress {
|
||||
// interface Chainable<Subject = any> {
|
||||
// customCommand(param: any): typeof customCommand;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// function customCommand(param: any): void {
|
||||
// console.warn(param);
|
||||
// }
|
||||
//
|
||||
// NOTE: You can use it like so:
|
||||
// Cypress.Commands.add('customCommand', customCommand);
|
||||
//
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-namespace,@typescript-eslint/no-explicit-any */
|
||||
import type {Component} from '@angular/core';
|
||||
import {
|
||||
interceptBackend,
|
||||
interceptConfig,
|
||||
interceptGet,
|
||||
interceptMultiSearch,
|
||||
interceptSearch,
|
||||
} from './commands/backend';
|
||||
import {component, ng, runInsideAngular, zone} from './commands/angular';
|
||||
import {
|
||||
clearAllSettings,
|
||||
getAllSettings,
|
||||
getSetting,
|
||||
setLocalConfig,
|
||||
setSettings,
|
||||
storage,
|
||||
} from './commands/settings';
|
||||
import {patchSearchPage} from './commands/patches';
|
||||
|
||||
const commands = {
|
||||
interceptConfig,
|
||||
interceptBackend,
|
||||
interceptSearch,
|
||||
interceptMultiSearch,
|
||||
interceptGet,
|
||||
storage,
|
||||
setLocalConfig,
|
||||
setSettings,
|
||||
getSetting,
|
||||
clearAllSettings,
|
||||
getAllSettings,
|
||||
patchSearchPage,
|
||||
ng,
|
||||
zone,
|
||||
};
|
||||
|
||||
const childCommands = {
|
||||
component,
|
||||
runInsideAngular,
|
||||
};
|
||||
|
||||
Cypress.Commands.addAll(commands);
|
||||
Cypress.Commands.addAll({prevSubject: true}, childCommands);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// items that include generics also have to be defined here separately
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface Chainable extends CustomCommands, CustomChildCommands {
|
||||
component<T = Component>(): Cypress.Chainable<T>;
|
||||
|
||||
runInsideAngular<T = any, U = void>(zoneAwareTask: (subject: T) => U): Cypress.Chainable<U>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CustomCommands = {
|
||||
[KEY in keyof typeof commands]: (
|
||||
...parameters: Parameters<(typeof commands)[KEY]>
|
||||
) => ChainableReturnType<(typeof commands)[KEY]>;
|
||||
};
|
||||
type OmitFirstArgument<F> = F extends (x: any, ...arguments_: infer P) => infer R
|
||||
? (...arguments_: P) => R
|
||||
: never;
|
||||
type CustomChildCommands = {
|
||||
[KEY in keyof typeof childCommands]: OmitFirstArgument<(typeof childCommands)[KEY]>;
|
||||
};
|
||||
type ChainableReturnType<T extends (...arguments_: any) => any> = ReturnType<T> extends Cypress.Chainable
|
||||
? ReturnType<T>
|
||||
: Cypress.Chainable<null>;
|
||||
|
||||
44
frontend/app/cypress/support/commands/angular.ts
Normal file
44
frontend/app/cypress/support/commands/angular.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {Component, NgZone} from '@angular/core';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function ng(): Cypress.Chainable<any> {
|
||||
return cy.window().its('ng');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Angular zone (Change Detection!)
|
||||
*/
|
||||
export function zone(): Cypress.Chainable<NgZone> {
|
||||
return cy.get('app-root').component().its('zone');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a callback inside Angular so change detection can happen
|
||||
*/
|
||||
export function runInsideAngular<T, U>(subject: T, zoneAwareTask: (subject: T) => U): Cypress.Chainable<U> {
|
||||
return cy.zone().then(zone => cy.wrap(zone.run(zoneAwareTask, undefined, [subject])));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function component<T = Component>($element: Cypress.JQueryWithSelector): Cypress.Chainable<T> {
|
||||
return cy.ng().then(ng => ng.getComponent($element[0]));
|
||||
}
|
||||
102
frontend/app/cypress/support/commands/backend.ts
Normal file
102
frontend/app/cypress/support/commands/backend.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import equal from 'fast-deep-equal';
|
||||
import {extendsDeepEqual} from '@openstapps/collection-utils';
|
||||
import {SCSearchRequest, SCSearchResponse} from '@openstapps/core';
|
||||
|
||||
type InterceptArguments = {
|
||||
fixture?: string | SCSearchResponse | ((request: SCSearchRequest) => SCSearchResponse);
|
||||
alias?: string;
|
||||
} & (
|
||||
| {
|
||||
exact: string | SCSearchRequest | ((searchRequest: SCSearchRequest) => boolean);
|
||||
}
|
||||
| {
|
||||
extends: string | SCSearchRequest;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function interceptConfig(config?: string) {
|
||||
cy.intercept({url: '/', method: 'POST'}, {fixture: config || 'config/default-config.json'}).as('config');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function interceptBackend(route: string, fixtureKey: string, parameters: InterceptArguments) {
|
||||
const condition = 'exact' in parameters ? parameters.exact : parameters.extends;
|
||||
parameters.fixture ||= 'no-results';
|
||||
|
||||
Cypress.Promise.all([
|
||||
typeof condition === 'string' ? cy.fixture(`${condition}.${fixtureKey}.req.json`) : condition,
|
||||
typeof parameters.fixture === 'string'
|
||||
? cy.fixture(`${parameters.fixture}.${fixtureKey}.res.json`)
|
||||
: parameters.fixture,
|
||||
'exact' in parameters ? equal : extendsDeepEqual,
|
||||
] as const).spread((requestCondition, response, comparisonFunction) => {
|
||||
cy.intercept({url: route, method: 'POST'}, request => {
|
||||
const body = request.body;
|
||||
|
||||
if (
|
||||
typeof requestCondition === 'object'
|
||||
? (comparisonFunction as any)(requestCondition, body)
|
||||
: (requestCondition as any)(body)
|
||||
) {
|
||||
request.alias = parameters.alias;
|
||||
|
||||
request.reply(typeof response === 'object' ? response : (response as any)(body));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function interceptSearch(parameters: InterceptArguments) {
|
||||
parameters.alias ||= 'search';
|
||||
cy.interceptBackend('/search', 'search', parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function interceptMultiSearch(parameters: InterceptArguments) {
|
||||
parameters.alias ||= 'multi-search';
|
||||
cy.interceptBackend('/search/multi', 'multi', parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function interceptGet(parameters: Omit<InterceptArguments, 'exact' | 'extends'> & {uid: string}) {
|
||||
cy.interceptBackend('/search', 'get', {
|
||||
exact: request => {
|
||||
return (
|
||||
request.size === 1 &&
|
||||
request.filter?.type === 'value' &&
|
||||
request.filter.arguments.field === 'uid' &&
|
||||
request.filter.arguments.value === parameters.uid
|
||||
);
|
||||
},
|
||||
fixture: parameters.fixture || `get/${parameters.uid}`,
|
||||
alias: parameters.alias || parameters.uid,
|
||||
});
|
||||
}
|
||||
29
frontend/app/cypress/support/commands/patches.ts
Normal file
29
frontend/app/cypress/support/commands/patches.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import type {SearchPageComponent} from '../../../src/app/modules/data/list/search-page.component';
|
||||
|
||||
/**
|
||||
* Sets the due time to zero, higher values can lead to issues in cypress
|
||||
*/
|
||||
export function patchSearchPage(dueTime = 0) {
|
||||
return cy
|
||||
.get('stapps-search-page')
|
||||
.component<SearchPageComponent>()
|
||||
.then(component => {
|
||||
component.searchQueryDueTime = dueTime;
|
||||
// component.ngOnDestroy();
|
||||
component.ngOnInit();
|
||||
});
|
||||
}
|
||||
63
frontend/app/cypress/support/commands/settings.ts
Normal file
63
frontend/app/cypress/support/commands/settings.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2023 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {STORAGE_KEY_SETTING_VALUES} from '../../../src/app/modules/settings/settings.provider';
|
||||
import {Storage} from '@ionic/storage-angular';
|
||||
import deepmerge from 'deepmerge';
|
||||
import {STORAGE_KEY_CONFIG} from '../../../src/app/modules/config/config.provider';
|
||||
import {SCIndexResponse} from '@openstapps/core';
|
||||
|
||||
export function storage(): Cypress.Chainable<Storage> {
|
||||
const storage = new Storage({});
|
||||
return cy.wrap(storage.create());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes settings
|
||||
*/
|
||||
export function setSettings(settings: Record<string, Record<string, string>>) {
|
||||
return cy.storage().then(async storage => {
|
||||
const currentSettings = (await storage.get(STORAGE_KEY_SETTING_VALUES)) || {};
|
||||
console.log(deepmerge(currentSettings, settings));
|
||||
await storage.set(STORAGE_KEY_SETTING_VALUES, deepmerge(currentSettings, settings));
|
||||
});
|
||||
}
|
||||
|
||||
export function getAllSettings(): Cypress.Chainable<Record<string, Record<string, string>> | undefined> {
|
||||
return cy.storage().invoke('get', STORAGE_KEY_SETTING_VALUES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads settings
|
||||
*/
|
||||
export function getSetting(group: string, key: string): Cypress.Chainable<string | undefined> {
|
||||
return cy.storage().then(async storage => {
|
||||
const currentSettings = await storage.get(STORAGE_KEY_SETTING_VALUES);
|
||||
return currentSettings?.[group]?.[key];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function clearAllSettings() {
|
||||
return cy.storage().invoke('clear');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function setLocalConfig(config: SCIndexResponse | any) {
|
||||
cy.storage().invoke('set', STORAGE_KEY_CONFIG, config);
|
||||
}
|
||||
@@ -12,38 +12,20 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
// import './commands';
|
||||
// JIT compiler for dependency mocking
|
||||
import '@angular/compiler';
|
||||
import './commands';
|
||||
|
||||
beforeEach(function () {
|
||||
cy.wrap(
|
||||
new Promise(resolve => {
|
||||
window.indexedDB.deleteDatabase('_ionicstorage').onsuccess = resolve;
|
||||
}),
|
||||
);
|
||||
cy.clearAllSettings();
|
||||
cy.setLocalConfig({});
|
||||
cy.getAllSettings().should('be.empty');
|
||||
cy.setSettings({profile: {language: 'de'}});
|
||||
cy.interceptConfig();
|
||||
cy.clock(new Date('2020-01-01T12:00:00.000Z'), ['Date']);
|
||||
});
|
||||
|
||||
Cypress.on('window:before:load', window => {
|
||||
// Fake that user is using its browser in German
|
||||
Object.defineProperty(window.navigator, 'language', {value: 'de-DE'});
|
||||
Object.defineProperty(window.navigator, 'languages', [{value: 'de-DE'}]);
|
||||
|
||||
cy.spy(window.console, 'error').as('consoleError');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user