feat: improved e2e tests

This commit is contained in:
2023-12-05 15:21:01 +01:00
committed by Thea Schöbl
parent bff2d985aa
commit d7a85b7fae
66 changed files with 6353 additions and 4471 deletions

View File

@@ -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>;

View 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]));
}

View 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,
});
}

View 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();
});
}

View 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);
}

View File

@@ -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');
});