@actionlogementservices/wdio-test-context 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.editorconfig +20 -0
  2. package/.jsdoc.json +35 -0
  3. package/.prettierrc +11 -0
  4. package/README.md +116 -0
  5. package/doc/images/badges/modifier-abstract.png +0 -0
  6. package/doc/images/badges/modifier-abstract.svg +10 -0
  7. package/doc/images/badges/modifier-default.png +0 -0
  8. package/doc/images/badges/modifier-default.svg +10 -0
  9. package/doc/images/badges/modifier-private.png +0 -0
  10. package/doc/images/badges/modifier-private.svg +10 -0
  11. package/doc/images/badges/modifier-protected.png +0 -0
  12. package/doc/images/badges/modifier-protected.svg +10 -0
  13. package/doc/images/badges/modifier-public.png +0 -0
  14. package/doc/images/badges/modifier-public.svg +10 -0
  15. package/doc/images/badges/modifier-readonly.png +0 -0
  16. package/doc/images/badges/modifier-readonly.svg +10 -0
  17. package/doc/images/badges/modifier-static.png +0 -0
  18. package/doc/images/badges/modifier-static.svg +10 -0
  19. package/doc/src-page-objects_guerrillamail.page.md +69 -0
  20. package/doc/src-page-objects_maildrop.page.md +57 -0
  21. package/doc/src-page-objects_page.md +484 -0
  22. package/doc/src-page-objects_yopmail.page.md +57 -0
  23. package/doc/src-wdio_ado-reporter.md +93 -0
  24. package/doc/src-wdio_test-context-worker-service.md +48 -0
  25. package/doc/src_ado-client.md +55 -0
  26. package/doc/src_logger.md +183 -0
  27. package/doc/src_mail-service.md +54 -0
  28. package/doc/src_pdf-generator.md +35 -0
  29. package/doc/src_test-context.md +173 -0
  30. package/doc/toc.md +22 -0
  31. package/eslint.config.js +100 -0
  32. package/jsconfig.json +14 -0
  33. package/package.json +58 -0
  34. package/src/ado-client.js +96 -0
  35. package/src/index.js +8 -0
  36. package/src/logger.js +125 -0
  37. package/src/mail-service.js +81 -0
  38. package/src/page-objects/guerrillamail.page.js +144 -0
  39. package/src/page-objects/index.js +3 -0
  40. package/src/page-objects/maildrop.page.js +97 -0
  41. package/src/page-objects/page.js +463 -0
  42. package/src/page-objects/yopmail.page.js +102 -0
  43. package/src/pdf-generator.js +61 -0
  44. package/src/test-context.js +248 -0
  45. package/src/types.d.ts +60 -0
  46. package/src/wdio/ado-reporter.js +111 -0
  47. package/src/wdio/test-context-worker-service.js +88 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Azure DevOps rest client to push test results.
3
+ * @module
4
+ * @category internal
5
+ */
6
+
7
+ import { WebApi, getPersonalAccessTokenHandler, getBearerHandler } from 'azure-devops-node-api';
8
+
9
+ import { logger } from './logger.js';
10
+
11
+ /** @typedef { import('azure-devops-node-api/TestApi.d.ts').ITestApi } ITestApi */
12
+ /** @typedef { { token: string, url: string, project: string } } ConnectionInfo */
13
+ /** @typedef { { testAgent: ITestApi } } Apis */
14
+ /**
15
+ * @typedef {Omit<import('azure-devops-node-api/interfaces/TestInterfaces.d.ts').RunCreateModel, "plan" | "configurationIds"> & import('azure-devops-node-api/interfaces/TestInterfaces.d.ts').RunUpdateModel & { results: import('azure-devops-node-api/interfaces/TestInterfaces.d.ts').TestCaseResult[]}} RunModel
16
+ */
17
+
18
+ /** @type {(input: string)=> boolean} */
19
+ const isNullOrEmpty = input => !input || !input.trim();
20
+
21
+ /**
22
+ * Retrieves Azure DevOps connection information.
23
+ * @returns {ConnectionInfo}
24
+ */
25
+ function getConnectionInfo() {
26
+ return {
27
+ token: process.env.SYSTEM_ACCESSTOKEN || process.env.TOKEN,
28
+ url: process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI,
29
+ project: process.env.SYSTEM_TEAMPROJECT
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Gets all necessary REST API clients.
35
+ * See https://github.com/Microsoft/azure-devops-node-api and https://docs.microsoft.com/en-us/rest/api/azure/devops/
36
+ * @param {ConnectionInfo} connectionInfo
37
+ * @returns {Promise<Apis>}
38
+ */
39
+ async function getApis(connectionInfo) {
40
+ if (isNullOrEmpty(connectionInfo.token))
41
+ throw new Error(
42
+ `Impossible de récupérer l'access token de l'agent via la variable d'environnement SYSTEM_ACCESSTOKEN.`
43
+ );
44
+ if (isNullOrEmpty(connectionInfo.url))
45
+ throw new Error(
46
+ `Impossible de récupérer l'url Azure DevOps via la variable d'environnement SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.`
47
+ );
48
+ if (isNullOrEmpty(connectionInfo.project))
49
+ throw new Error(
50
+ `Impossible de récupérer le projet Azure DevOps via la variable d'environnement SYSTEM_TEAMPROJECT.`
51
+ );
52
+ const patAuthentication = !isNullOrEmpty(process.env.TOKEN);
53
+ const connection = new WebApi(
54
+ connectionInfo.url,
55
+ patAuthentication
56
+ ? getPersonalAccessTokenHandler(connectionInfo.token)
57
+ : getBearerHandler(connectionInfo.token)
58
+ );
59
+ const testAgent = await connection.getTestApi();
60
+ return { testAgent };
61
+ }
62
+
63
+ /**
64
+ * Creates the Azure DevOps TestRun.
65
+ * @param {RunModel} testRun
66
+ */
67
+ async function createAdoTestRun({ name, startDate, comment, results, state, completedDate, build }) {
68
+ try {
69
+ // get REST API clients
70
+ const connectionInfo = getConnectionInfo();
71
+ const apis = await getApis(connectionInfo);
72
+ const testRun = await apis.testAgent.createTestRun(
73
+ // @ts-ignore
74
+ { automated: true, name, startDate, build, state: 'InProgress', comment },
75
+ connectionInfo.project
76
+ );
77
+ if (testRun === null) throw new Error('Vérifier les droits.');
78
+ await apis.testAgent.addTestResultsToTestRun(results, connectionInfo.project, testRun.id);
79
+ // en cas de suite Aborted il faut déjà la sauvegarder en Completed avant sinon tous les tests sont marqués à Failed
80
+ await apis.testAgent.updateTestRun(
81
+ { state: 'Completed', completedDate },
82
+ connectionInfo.project,
83
+ testRun.id
84
+ );
85
+ if (state === 'Aborted')
86
+ await apis.testAgent.updateTestRun(
87
+ { state: 'Aborted', completedDate },
88
+ connectionInfo.project,
89
+ testRun.id
90
+ );
91
+ } catch (error) {
92
+ logger.detailError(error);
93
+ }
94
+ }
95
+
96
+ export { createAdoTestRun };
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { AdoReporter } from './wdio/ado-reporter.js';
2
+ export { default as TestContextWorkerService } from './wdio/test-context-worker-service.js';
3
+
4
+ export { default as BasePage } from './page-objects/page.js';
5
+
6
+ export { default as pdfGenerator } from './pdf-generator.js';
7
+
8
+ export { TestContext } from './test-context.js';
package/src/logger.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Internal logger.
3
+ * @module
4
+ * @category internal
5
+ */
6
+
7
+ /* eslint-disable no-console */
8
+ import chalk from 'chalk';
9
+
10
+ const noop = () => {};
11
+ /** console implementation @type {(message: string) => void} */
12
+ const logError = message => console.error(chalk.red(message));
13
+ /** console implementation @type {(message: string) => void} */
14
+ const logWarn = message => console.warn(chalk.yellow(message));
15
+ /** console implementation @type {(message: string) => void} */
16
+ const logInfo = message => console.info(chalk.bold.green(message));
17
+ /** console implementation @type {(message: string) => void} */
18
+ const logDebug = message => console.debug(chalk.blueBright.italic(message));
19
+ /** console implementation @type {(message: string) => void} */
20
+ const log = message => console.log(message);
21
+
22
+ /** @type {(message: string) => string} */
23
+ const bold = text => chalk.bold(text);
24
+
25
+ const level = {
26
+ none: 0,
27
+ error: 1,
28
+ warn: 2,
29
+ info: 3,
30
+ debug: 4
31
+ };
32
+
33
+ class Logger {
34
+ /** @type {Map<'error' | 'warn' | 'info' | 'debug' | 'log', (message: string) => void>} */ _logMethodFactory;
35
+ /** @type {Number} */ _level;
36
+
37
+ /**
38
+ * Implements an internal Logger plugged on console.
39
+ * @param {import('./types').LogLevel} logLevel logging level
40
+ */
41
+ constructor(logLevel) {
42
+ this.setLogLevel(logLevel);
43
+ }
44
+
45
+ /**
46
+ * Sets the logging level.
47
+ * @param {import('./types').LogLevel} logLevel logging level
48
+ */
49
+ setLogLevel(logLevel) {
50
+ this._level = level[logLevel] ?? level.none;
51
+ this._logMethodFactory = new Map();
52
+ this._logMethodFactory.set('error', this._level >= level.error ? logError : noop);
53
+ this._logMethodFactory.set('warn', this._level >= level.warn ? logWarn : noop);
54
+ this._logMethodFactory.set('info', this._level >= level.info ? logInfo : noop);
55
+ this._logMethodFactory.set('debug', this._level >= level.debug ? logDebug : noop);
56
+ this._logMethodFactory.set('log', this._level >= level.debug ? log : noop);
57
+ }
58
+
59
+ /**
60
+ * Logs an **informational** message.
61
+ * @param {string} message the message to log
62
+ */
63
+ info(message) {
64
+ this._logMethodFactory.get('info')(message);
65
+ }
66
+
67
+ /**
68
+ * Logs a message.
69
+ * @param {string} message the message to log
70
+ */
71
+ log(message) {
72
+ this._logMethodFactory.get('log')(message);
73
+ }
74
+
75
+ /**
76
+ * Logs a **warning** message.
77
+ * @param {string} message the message to log
78
+ */
79
+ warn(message) {
80
+ this._logMethodFactory.get('warn')(message);
81
+ }
82
+
83
+ /**
84
+ * Logs an **error** message.
85
+ * @param {string} message the message to log
86
+ */
87
+ error(message) {
88
+ this._logMethodFactory.get('error')(message);
89
+ }
90
+
91
+ /**
92
+ * Logs a **debug** message
93
+ * @param {string} message the message to log
94
+ */
95
+ debug(message) {
96
+ this._logMethodFactory.get('debug')(message);
97
+ }
98
+
99
+ /**
100
+ * Logs an **error** message based on the specified error object.
101
+ * @param {Error} detailedError the error object to log
102
+ */
103
+ detailError(detailedError) {
104
+ if (this._level === level.none) return;
105
+ const { message, stack } = detailedError;
106
+ console.error('Error: ' + chalk.red.bold(message));
107
+ if (stack) console.error('Stack: ' + chalk.gray(stack));
108
+ }
109
+
110
+ /**
111
+ * Logs an **warning** message based on the specified error object.
112
+ * @param {string} text the text message to log
113
+ * @param {{ message: string, errorType: string }} meaningfulErrorInfo the meaningfull error info to log
114
+ */
115
+ warnError(text, meaningfulErrorInfo) {
116
+ if (this._level < level.warn) return;
117
+ const { message, errorType } = meaningfulErrorInfo;
118
+ logWarn(`${text}${bold(errorType)}: ${message}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The common logger instance.
124
+ */
125
+ export const logger = new Logger('error');
@@ -0,0 +1,81 @@
1
+ /**
2
+ * **Mail service** to send and retrieve mail from various provider.
3
+ * Accessible through the **mailService property** of the TestContext instance.
4
+ * @module
5
+ * @category public
6
+ */
7
+
8
+ import nodemailer from 'nodemailer';
9
+
10
+ import * as provider from './page-objects/index.js';
11
+ import { logger } from './logger.js';
12
+
13
+ /** @typedef {import('./types.js').MailProvider} MailProvider */
14
+
15
+ export class MailService {
16
+ /** disposable email provider @type {MailProvider} */ provider;
17
+
18
+ /**
19
+ * Implements a service that sends and retrieves emails with a given provider.
20
+ * @param {import('./types.js').MailProviderName} providerName disposable email provider name
21
+ */
22
+ constructor(providerName) {
23
+ this.provider = provider[providerName].default;
24
+ }
25
+
26
+ /**
27
+ * Provider name
28
+ * @type {string}
29
+ */
30
+ get name() {
31
+ return this.provider?.name;
32
+ }
33
+
34
+ /**
35
+ * Emails domains exposed by the provider.
36
+ * @type {string[]}
37
+ */
38
+ get domains() {
39
+ return this.provider?.domains;
40
+ }
41
+
42
+ /**
43
+ * Sends a mail.
44
+ * @param {string} from sender
45
+ * @param {string} to recipient
46
+ * @param {string} subject subject of the mail
47
+ * @param {string} html html body string of the mail
48
+ * @returns {Promise<void>}
49
+ */
50
+ async sendMessage(from, to, subject, html) {
51
+ const transporter = nodemailer.createTransport({ host: 'smtp.als.lan', secure: false, port: 25 });
52
+ await transporter.sendMail({ from, to, subject, html });
53
+ }
54
+
55
+ /**
56
+ * Waits for an email from the specified sender and containing the specified subject
57
+ * and gets its content.
58
+ * @param {string} email recipient email
59
+ * @param {string} from sender
60
+ * @param {string} subject subject of email
61
+ * @returns {Promise<string>} content of email
62
+ */
63
+ async waitForMessage(email, from, subject) {
64
+ logger.debug('1/3 - Opening mailbox...');
65
+ await this.provider.openInbox(email);
66
+ logger.debug('2/3 - Looking for mails...');
67
+ const messages = await this.provider.getMessages();
68
+ logger.debug(` - ${messages.length} mails in inbox.`);
69
+ const correspondingIndex = messages.findIndex(
70
+ m =>
71
+ m.from.toLowerCase().includes(from.toLowerCase()) &&
72
+ m.subject.toLowerCase().includes(subject.toLowerCase())
73
+ );
74
+ if (correspondingIndex === -1) {
75
+ logger.warn(' No corresponding mail found!');
76
+ return;
77
+ }
78
+ logger.debug('3/3 - Mail found, extracting content...');
79
+ return this.provider.getMessageContent(correspondingIndex);
80
+ }
81
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Page-object on top of Guerilla Mail.
3
+ * @module
4
+ * @category internal
5
+ */
6
+
7
+ /* global browser,$,$$ */
8
+
9
+ import Page from './page.js';
10
+ import { logger } from '../logger.js';
11
+
12
+ const mailFromGuerilla = 'no-reply@guerrillamail.com';
13
+
14
+ /**
15
+ * Implements a page-object on top of Guerilla Mail.
16
+ */
17
+ class GuerillamailPage extends Page {
18
+ /** name of the provider */ name = 'Guerilla Mail';
19
+
20
+ /** domains exposed of the provider */
21
+ domains = [
22
+ 'sharklasers.com',
23
+ 'guerrillamail.info',
24
+ 'grr.la',
25
+ 'guerrillamail.biz',
26
+ 'guerrillamail.com',
27
+ 'guerrillamail.de',
28
+ 'guerrillamail.net',
29
+ 'guerrillamail.org',
30
+ 'guerrillamailblock.com',
31
+ 'pokemail.net',
32
+ 'spam4.me'
33
+ ];
34
+
35
+ css = {
36
+ text: {
37
+ inbox: '#content div.mailbox table tbody'
38
+ },
39
+ input: {
40
+ nom: '#inbox-id input'
41
+ },
42
+ button: {
43
+ nom: '#inbox-id',
44
+ set: 'button.save',
45
+ consent: 'div.fc-consent-root button[aria-label=Consent]'
46
+ },
47
+ options: {
48
+ domaines: '#gm-host-select option'
49
+ },
50
+ tbody: {
51
+ messages: '#email_list'
52
+ },
53
+ select: {
54
+ domaine: '#gm-host-select'
55
+ },
56
+ div: {
57
+ messages: '#email_list tr',
58
+ content: 'div.email_body',
59
+ popup: 'div.fc-consent-root'
60
+ },
61
+ td: {
62
+ from: 'td:nth-of-type(2)',
63
+ fromguerrilla: '#email_list tr:nth-of-type(1) td.td2',
64
+ subject: 'td:nth-of-type(3)'
65
+ }
66
+ };
67
+
68
+ /**
69
+ * WDIO logic to open the mailbox.
70
+ * @param {string} email disposable email
71
+ */
72
+ async openInbox(email) {
73
+ await this.openNewTab('inbox', `https://www.guerrillamail.com/fr/inbox`);
74
+ const popup = await this.verifyElementExistsDuring(this.css.div.popup, 5);
75
+ if (popup) {
76
+ await this.click(this.css.button.consent);
77
+ logger.debug('Guerilla Mail popup detected');
78
+ }
79
+ const [alias, domain] = email.split('@');
80
+ await this.click(this.css.button.nom);
81
+ await this.fillInput(this.css.input.nom, alias);
82
+ await this.click(this.css.button.set);
83
+ await this.selectItem(this.css.select.domaine, domain);
84
+ }
85
+
86
+ /**
87
+ * WDIO logic to retrieve domains exposed by the provider.
88
+ * @returns {Promise<string[]>}
89
+ */
90
+ async retrieveDomains() {
91
+ // le tableau contenant tous les domaines
92
+ const domains = await this.getOptions(this.css.options.domaines);
93
+ return domains;
94
+ }
95
+
96
+ /**
97
+ * WDIO logic to get emails.
98
+ * @return {Promise<{ from: string, subject: string}[]>}
99
+ */
100
+ async getMessages() {
101
+ let noMessage = true;
102
+ do {
103
+ // on attend 1s avant de revérifier
104
+ await browser.pause(1000);
105
+ // on vérifie s'il y a des messages autre que celui de guerilla
106
+ const from = await $(this.css.td.fromguerrilla).getText();
107
+ noMessage = from === mailFromGuerilla;
108
+ } while (noMessage);
109
+ const froms = [];
110
+ const messagesCount = await $$(this.css.div.messages).length;
111
+ for (let index = 0; index < messagesCount; index++) {
112
+ const from = await $(`tr:nth-of-type(${index + 1}) td:nth-of-type(2)`).getText();
113
+ const subject = await $(`tr:nth-of-type(${index + 1}) td:nth-of-type(3)`).getText();
114
+ froms.push({ from, subject });
115
+ }
116
+ // // étrangement, ne fonctionne pas avec les tableaux
117
+ // const messages = await $$(this.css.div.messages).getElements();
118
+ // for (const message of messages) {
119
+ // const from = await message.$(this.css.td.from).getText();
120
+ // const subject = await message.$(this.css.td.subject).getText();
121
+ // froms.push({ from, subject });
122
+ // }
123
+ return froms;
124
+ }
125
+
126
+ /**
127
+ * WDIO logic to get the specified message content.
128
+ * @param {number} index
129
+ * @returns {Promise<string>}
130
+ */
131
+ async getMessageContent(index) {
132
+ // on attend que la zone contenant les messages soit prête
133
+ await $(this.css.div.messages).parentElement().waitForStable();
134
+ const messages = await $$(this.css.div.messages);
135
+ const message = messages[index];
136
+ await message.click();
137
+ const content = await $(this.css.div.content);
138
+ return content.getHTML({ includeSelectorTag: false });
139
+ }
140
+ }
141
+
142
+ const page = new GuerillamailPage();
143
+
144
+ export default page;
@@ -0,0 +1,3 @@
1
+ export * as guerrillamail from './guerrillamail.page.js';
2
+ export * as yopmail from './yopmail.page.js';
3
+ export * as maildrop from './maildrop.page.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Page-object on top of maildrop.
3
+ * @module
4
+ * @category internal
5
+ */
6
+
7
+ /* global browser,$,$$ */
8
+
9
+ import Page from './page.js';
10
+
11
+ /**
12
+ * Implements a page-object on top of maildrop.
13
+ */
14
+ class MaildropPage extends Page {
15
+ /** name of the provider */ name = 'maildrop';
16
+
17
+ /** domains exposed of the provider */ domains = ['maildrop.cc'];
18
+
19
+ css = {
20
+ text: {
21
+ inbox: '#content div.mailbox table tbody'
22
+ },
23
+ div: {
24
+ messageZone: 'main div.subhead',
25
+ messages: 'main div.message',
26
+ from: 'div:nth-of-type(1)',
27
+ subject: 'div:nth-of-type(3)',
28
+ content: 'html body'
29
+ },
30
+ input: {
31
+ name: 'main + section form input[placeholder=view-this-mailbox]'
32
+ },
33
+ button: {
34
+ view: 'main + section form button',
35
+ refresh: 'main button'
36
+ },
37
+ iframe: {
38
+ messages: 'iframe[srcdoc]'
39
+ }
40
+ };
41
+
42
+ /**
43
+ * WDIO logic to open the mailbox.
44
+ * @param {string} email disposable email
45
+ */
46
+ async openInbox(email) {
47
+ await this.openNewTab('inbox', `https://maildrop.cc`);
48
+ const alias = email.split('@')[0];
49
+ // on clique dans le champ sinon le site change son contenu quand on clique sur le bouton View
50
+ await this.click(this.css.input.name);
51
+ await browser.pause(400);
52
+ await this.fillInput(this.css.input.name, alias);
53
+ await browser.pause(300);
54
+ await this.click(this.css.button.view);
55
+ await $(this.css.button.refresh).waitForExist();
56
+ }
57
+
58
+ /**
59
+ * WDIO logic to get emails.
60
+ * @return {Promise<{ from: string, subject: string}[]>}
61
+ */
62
+ async getMessages() {
63
+ let noMessage = true;
64
+ do {
65
+ // on attend 1,5s avant de cliquer sur refresh
66
+ await this.click(this.css.button.refresh);
67
+ // on attend que la zone contenant les messages soit prête
68
+ await $(this.css.div.messageZone).waitForDisplayed();
69
+ // on vérifie s'il y a des messages
70
+ noMessage = !(await this.verifyElementExists(this.css.div.messages));
71
+ if (noMessage) await browser.pause(1500);
72
+ } while (noMessage);
73
+ const messages = await $$(this.css.div.messages).getElements();
74
+ const froms = [];
75
+ for (const message of messages) {
76
+ const from = await message.$(this.css.div.from).getText();
77
+ const subject = await message.$(this.css.div.subject).getText();
78
+ froms.push({ from, subject });
79
+ }
80
+ return froms;
81
+ }
82
+
83
+ /**
84
+ * WDIO logic to get the specified message content.
85
+ * @param {number} index
86
+ * @returns {Promise<string>}
87
+ */
88
+ async getMessageContent(index) {
89
+ await this.click(`${this.css.div.messages}:nth-of-type(${index + 1})`);
90
+ await this.switchToFrame(this.css.iframe.messages);
91
+ return this.getHtml(this.css.div.content);
92
+ }
93
+ }
94
+
95
+ const page = new MaildropPage();
96
+
97
+ export default page;