Merge branch 'master' into access
Conflicts: test/e2e/lib/worker-counter.js
This commit is contained in:
		
						commit
						cda93630ea
					
				
					 25 changed files with 798 additions and 335 deletions
				
			
		|  | @ -16,7 +16,7 @@ let _ = require('../translate')._; | |||
| let util = require('util'); | ||||
| let tableHelpers = require('../table-helpers'); | ||||
| 
 | ||||
| let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled']; | ||||
| let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'click_tracking_disabled', 'open_tracking_disabled']; | ||||
| 
 | ||||
| module.exports.list = (start, limit, callback) => { | ||||
|     tableHelpers.list('campaigns', ['*'], 'scheduled', null, start, limit, callback); | ||||
|  | @ -370,7 +370,8 @@ module.exports.create = (campaign, opts, callback) => { | |||
|     campaign = tools.convertKeys(campaign); | ||||
|     let name = (campaign.name || '').toString().trim(); | ||||
| 
 | ||||
|     campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0; | ||||
|     campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0; | ||||
|     campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0; | ||||
| 
 | ||||
|     opts = opts || {}; | ||||
| 
 | ||||
|  | @ -592,7 +593,8 @@ module.exports.update = (id, updates, callback) => { | |||
|     let campaign = tools.convertKeys(updates); | ||||
|     let name = (campaign.name || '').toString().trim(); | ||||
| 
 | ||||
|     campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0; | ||||
|     campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0; | ||||
|     campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0; | ||||
| 
 | ||||
|     if (!name) { | ||||
|         return callback(new Error(_('Campaign Name must be set'))); | ||||
|  | @ -827,7 +829,7 @@ module.exports.reset = (id, callback) => { | |||
|                 return callback(err); | ||||
|             } | ||||
| 
 | ||||
|             connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0 WHERE id=? LIMIT 1', [id], err => { | ||||
|             connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0, `blacklisted`=0 WHERE id=? LIMIT 1', [id], err => { | ||||
|                 if (err) { | ||||
|                     connection.release(); | ||||
|                     return callback(err); | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ module.exports.countClick = (remoteIp, useragent, campaignCid, listCid, subscrip | |||
|             return callback(err); | ||||
|         } | ||||
| 
 | ||||
|         if (!data || data.campaign.trackingDisabled) { | ||||
|         if (!data || data.campaign.clickTrackingDisabled) { | ||||
|             return callback(null, false); | ||||
|         } | ||||
| 
 | ||||
|  | @ -158,7 +158,7 @@ module.exports.countOpen = (remoteIp, useragent, campaignCid, listCid, subscript | |||
|             return callback(err); | ||||
|         } | ||||
| 
 | ||||
|         if (!data || data.campaign.trackingDisabled) { | ||||
|         if (!data || data.campaign.openTrackingDisabled) { | ||||
|             return callback(null, false); | ||||
|         } | ||||
| 
 | ||||
|  | @ -268,56 +268,64 @@ module.exports.add = (url, campaignId, callback) => { | |||
| }; | ||||
| 
 | ||||
| module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => { | ||||
|     if (campaign.trackingDisabled || !message || !message.trim()) { | ||||
|     if ((campaign.openTrackingDisabled && campaign.clickTrackingDisabled) || !message || !message.trim()) { | ||||
|         // tracking is disabled, do not modify the message
 | ||||
|         return setImmediate(() => callback(null, message)); | ||||
|     } | ||||
|     let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi; | ||||
|     let urls = new Set(); | ||||
|     (message || '').replace(re, (match, prefix, url) => { | ||||
|         urls.add(url); | ||||
|     }); | ||||
| 
 | ||||
|     let map = new Map(); | ||||
|     let vals = urls.values(); | ||||
| 
 | ||||
|      | ||||
|     // insert tracking image
 | ||||
|     let inserted = false; | ||||
|     let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid))); | ||||
|     let img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">'; | ||||
|     message = message.replace(/<\/body\b/i, match => { | ||||
|         inserted = true; | ||||
|         return img + match; | ||||
|     }); | ||||
|     if (!inserted) { | ||||
|         message = message + img; | ||||
|     } | ||||
| 
 | ||||
|     let replaceUrls = () => { | ||||
|         callback(null, | ||||
|             message.replace(re, (match, prefix, url) => | ||||
|                 prefix + (map.has(url) ? urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid), encodeURIComponent(map.get(url)))) : url))); | ||||
|     }; | ||||
| 
 | ||||
|     let storeNext = () => { | ||||
|         let urlItem = vals.next(); | ||||
|         if (urlItem.done) { | ||||
|             return replaceUrls(); | ||||
|         } | ||||
| 
 | ||||
|         module.exports.add(he.decode(urlItem.value, { | ||||
|             isAttributeValue: true | ||||
|         }), campaign.id, (err, linkId, cid) => { | ||||
|             if (err) { | ||||
|                 log.error('Link', err); | ||||
|                 return storeNext(); | ||||
|             } | ||||
|             map.set(urlItem.value, cid); | ||||
|             return storeNext(); | ||||
|     if (!campaign.openTrackingDisabled) { | ||||
|         let inserted = false; | ||||
|         let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid))); | ||||
|         let img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">'; | ||||
|         message = message.replace(/<\/body\b/i, match => { | ||||
|             inserted = true; | ||||
|             return img + match; | ||||
|         }); | ||||
|         if (!inserted) { | ||||
|             message = message + img; | ||||
|         } | ||||
|         if (campaign.clickTrackingDisabled) { | ||||
|             return callback(null, message); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     if (!campaign.clickTrackingDisabled) { | ||||
|         let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi; | ||||
|         let urls = new Set(); | ||||
|         (message || '').replace(re, (match, prefix, url) => { | ||||
|             urls.add(url); | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     storeNext(); | ||||
|         let map = new Map(); | ||||
|         let vals = urls.values(); | ||||
| 
 | ||||
|         let replaceUrls = () => { | ||||
|             callback(null, | ||||
|                 message.replace(re, (match, prefix, url) => | ||||
|                     prefix + (map.has(url) ? urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid), encodeURIComponent(map.get(url)))) : url))); | ||||
|         }; | ||||
| 
 | ||||
|         let storeNext = () => { | ||||
|             let urlItem = vals.next(); | ||||
|             if (urlItem.done) { | ||||
|                 return replaceUrls(); | ||||
|             } | ||||
| 
 | ||||
|             module.exports.add(he.decode(urlItem.value, { | ||||
|                 isAttributeValue: true | ||||
|             }), campaign.id, (err, linkId, cid) => { | ||||
|                 if (err) { | ||||
|                     log.error('Link', err); | ||||
|                     return storeNext(); | ||||
|                 } | ||||
|                 map.set(urlItem.value, cid); | ||||
|                 return storeNext(); | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         storeNext(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| function getSubscriptionData(campaignCid, listCid, subscriptionCid, callback) { | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| { | ||||
|     "schemaVersion": 28 | ||||
|     "schemaVersion": 29 | ||||
| } | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ exports.up = function(knex, Promise) { | |||
|     // For now, we just check whether our DB is up-to-date based on the existing SQL migration infrastructure in Mailtrain.
 | ||||
|     return knex('settings').where({key: 'db_schema_version'}).first('value') | ||||
|         .then(row => { | ||||
|             if (!row || Number(row.value) !== 28) { | ||||
|             if (!row || Number(row.value) !== 29) { | ||||
|                 throw new Error('Unsupported DB schema version: ' + row.value); | ||||
|             } | ||||
|         }) | ||||
|  |  | |||
|  | @ -69,7 +69,6 @@ CREATE TABLE `campaigns` ( | |||
|   `html_prepared` longtext, | ||||
|   `text` longtext, | ||||
|   `status` tinyint(4) unsigned NOT NULL DEFAULT '1', | ||||
|   `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `scheduled` timestamp NULL DEFAULT NULL, | ||||
|   `status_change` timestamp NULL DEFAULT NULL, | ||||
|   `delivered` int(11) unsigned NOT NULL DEFAULT '0', | ||||
|  | @ -80,6 +79,8 @@ CREATE TABLE `campaigns` ( | |||
|   `bounced` int(1) unsigned NOT NULL DEFAULT '0', | ||||
|   `complained` int(1) unsigned NOT NULL DEFAULT '0', | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `open_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `click_tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `name` (`name`(191)), | ||||
|  | @ -93,8 +94,8 @@ CREATE TABLE `confirmations` ( | |||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `cid` varchar(255) CHARACTER SET ascii NOT NULL, | ||||
|   `list` int(11) unsigned NOT NULL, | ||||
|   `email` varchar(255) NOT NULL, | ||||
|   `opt_in_ip` varchar(100) DEFAULT NULL, | ||||
|   `action` varchar(100) NOT NULL, | ||||
|   `ip` varchar(100) DEFAULT NULL, | ||||
|   `data` text NOT NULL, | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   PRIMARY KEY (`id`), | ||||
|  | @ -193,11 +194,17 @@ CREATE TABLE `lists` ( | |||
|   `subscribers` int(11) unsigned DEFAULT '0', | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `public_subscribe` tinyint(1) unsigned NOT NULL DEFAULT '1', | ||||
|   `unsubscription_mode` int(11) unsigned NOT NULL DEFAULT '0', | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `name` (`name`(191)) | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`) VALUES (1,'Hkj1vCoJb',NULL,'01 Testlist - Public Subscribe','',0,NOW(),1); | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4; | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (1,'Hkj1vCoJb',0,'#1 (one-step, no form)','',0,NOW(),1,0); | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (2,'SktV4HDZ-',NULL,'#2 (one-step, with form)','',0,NOW(),1,1); | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (3,'BkdvNBw-W',NULL,'#3 (two-step, no form)','',0,NOW(),1,2); | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (4,'rJMKVrDZ-',NULL,'#4 (two-step, with form)','',0,NOW(),1,3); | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (5,'SJgoNSw-W',NULL,'#5 (manual unsubscribe)','',0,NOW(),1,4); | ||||
| INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`, `unsubscription_mode`) VALUES (6,'HyveEPvWW',NULL,'#6 (non-public)','',0,NOW(),0,0); | ||||
| CREATE TABLE `queued` ( | ||||
|   `campaign` int(11) unsigned NOT NULL, | ||||
|   `list` int(11) unsigned NOT NULL, | ||||
|  | @ -269,7 +276,7 @@ CREATE TABLE `settings` ( | |||
|   `value` text NOT NULL, | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `key` (`key`) | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4; | ||||
| ) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb4; | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','5587'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','NONE'); | ||||
|  | @ -286,7 +293,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes | |||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','https://mailtrain.org'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','27'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','29'); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (46,'ua_code',''); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (47,'shoutout',''); | ||||
| INSERT INTO `settings` (`id`, `key`, `value`) VALUES (54,'mail_transport','smtp'); | ||||
|  | @ -361,6 +368,146 @@ CREATE TABLE `subscription__1` ( | |||
|   KEY `latest_click` (`latest_click`), | ||||
|   KEY `created` (`created`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||||
| CREATE TABLE `subscription__2` ( | ||||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `cid` varchar(255) CHARACTER SET ascii NOT NULL, | ||||
|   `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', | ||||
|   `opt_in_ip` varchar(100) DEFAULT NULL, | ||||
|   `opt_in_country` varchar(2) DEFAULT NULL, | ||||
|   `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, | ||||
|   `imported` int(11) unsigned DEFAULT NULL, | ||||
|   `status` tinyint(4) unsigned NOT NULL DEFAULT '1', | ||||
|   `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `status_change` timestamp NULL DEFAULT NULL, | ||||
|   `latest_open` timestamp NULL DEFAULT NULL, | ||||
|   `latest_click` timestamp NULL DEFAULT NULL, | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `first_name` varchar(255) DEFAULT NULL, | ||||
|   `last_name` varchar(255) DEFAULT NULL, | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `email` (`email`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `status` (`status`), | ||||
|   KEY `first_name` (`first_name`(191)), | ||||
|   KEY `last_name` (`last_name`(191)), | ||||
|   KEY `subscriber_tz` (`tz`), | ||||
|   KEY `is_test` (`is_test`), | ||||
|   KEY `latest_open` (`latest_open`), | ||||
|   KEY `latest_click` (`latest_click`), | ||||
|   KEY `created` (`created`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||||
| CREATE TABLE `subscription__3` ( | ||||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `cid` varchar(255) CHARACTER SET ascii NOT NULL, | ||||
|   `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', | ||||
|   `opt_in_ip` varchar(100) DEFAULT NULL, | ||||
|   `opt_in_country` varchar(2) DEFAULT NULL, | ||||
|   `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, | ||||
|   `imported` int(11) unsigned DEFAULT NULL, | ||||
|   `status` tinyint(4) unsigned NOT NULL DEFAULT '1', | ||||
|   `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `status_change` timestamp NULL DEFAULT NULL, | ||||
|   `latest_open` timestamp NULL DEFAULT NULL, | ||||
|   `latest_click` timestamp NULL DEFAULT NULL, | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `first_name` varchar(255) DEFAULT NULL, | ||||
|   `last_name` varchar(255) DEFAULT NULL, | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `email` (`email`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `status` (`status`), | ||||
|   KEY `first_name` (`first_name`(191)), | ||||
|   KEY `last_name` (`last_name`(191)), | ||||
|   KEY `subscriber_tz` (`tz`), | ||||
|   KEY `is_test` (`is_test`), | ||||
|   KEY `latest_open` (`latest_open`), | ||||
|   KEY `latest_click` (`latest_click`), | ||||
|   KEY `created` (`created`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||||
| CREATE TABLE `subscription__4` ( | ||||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `cid` varchar(255) CHARACTER SET ascii NOT NULL, | ||||
|   `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', | ||||
|   `opt_in_ip` varchar(100) DEFAULT NULL, | ||||
|   `opt_in_country` varchar(2) DEFAULT NULL, | ||||
|   `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, | ||||
|   `imported` int(11) unsigned DEFAULT NULL, | ||||
|   `status` tinyint(4) unsigned NOT NULL DEFAULT '1', | ||||
|   `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `status_change` timestamp NULL DEFAULT NULL, | ||||
|   `latest_open` timestamp NULL DEFAULT NULL, | ||||
|   `latest_click` timestamp NULL DEFAULT NULL, | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `first_name` varchar(255) DEFAULT NULL, | ||||
|   `last_name` varchar(255) DEFAULT NULL, | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `email` (`email`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `status` (`status`), | ||||
|   KEY `first_name` (`first_name`(191)), | ||||
|   KEY `last_name` (`last_name`(191)), | ||||
|   KEY `subscriber_tz` (`tz`), | ||||
|   KEY `is_test` (`is_test`), | ||||
|   KEY `latest_open` (`latest_open`), | ||||
|   KEY `latest_click` (`latest_click`), | ||||
|   KEY `created` (`created`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||||
| CREATE TABLE `subscription__5` ( | ||||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `cid` varchar(255) CHARACTER SET ascii NOT NULL, | ||||
|   `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', | ||||
|   `opt_in_ip` varchar(100) DEFAULT NULL, | ||||
|   `opt_in_country` varchar(2) DEFAULT NULL, | ||||
|   `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, | ||||
|   `imported` int(11) unsigned DEFAULT NULL, | ||||
|   `status` tinyint(4) unsigned NOT NULL DEFAULT '1', | ||||
|   `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `status_change` timestamp NULL DEFAULT NULL, | ||||
|   `latest_open` timestamp NULL DEFAULT NULL, | ||||
|   `latest_click` timestamp NULL DEFAULT NULL, | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `first_name` varchar(255) DEFAULT NULL, | ||||
|   `last_name` varchar(255) DEFAULT NULL, | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `email` (`email`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `status` (`status`), | ||||
|   KEY `first_name` (`first_name`(191)), | ||||
|   KEY `last_name` (`last_name`(191)), | ||||
|   KEY `subscriber_tz` (`tz`), | ||||
|   KEY `is_test` (`is_test`), | ||||
|   KEY `latest_open` (`latest_open`), | ||||
|   KEY `latest_click` (`latest_click`), | ||||
|   KEY `created` (`created`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||||
| CREATE TABLE `subscription__6` ( | ||||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `cid` varchar(255) CHARACTER SET ascii NOT NULL, | ||||
|   `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', | ||||
|   `opt_in_ip` varchar(100) DEFAULT NULL, | ||||
|   `opt_in_country` varchar(2) DEFAULT NULL, | ||||
|   `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL, | ||||
|   `imported` int(11) unsigned DEFAULT NULL, | ||||
|   `status` tinyint(4) unsigned NOT NULL DEFAULT '1', | ||||
|   `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0', | ||||
|   `status_change` timestamp NULL DEFAULT NULL, | ||||
|   `latest_open` timestamp NULL DEFAULT NULL, | ||||
|   `latest_click` timestamp NULL DEFAULT NULL, | ||||
|   `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|   `first_name` varchar(255) DEFAULT NULL, | ||||
|   `last_name` varchar(255) DEFAULT NULL, | ||||
|   PRIMARY KEY (`id`), | ||||
|   UNIQUE KEY `email` (`email`), | ||||
|   UNIQUE KEY `cid` (`cid`), | ||||
|   KEY `status` (`status`), | ||||
|   KEY `first_name` (`first_name`(191)), | ||||
|   KEY `last_name` (`last_name`(191)), | ||||
|   KEY `subscriber_tz` (`tz`), | ||||
|   KEY `is_test` (`is_test`), | ||||
|   KEY `latest_open` (`latest_open`), | ||||
|   KEY `latest_click` (`latest_click`), | ||||
|   KEY `created` (`created`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; | ||||
| CREATE TABLE `templates` ( | ||||
|   `id` int(11) unsigned NOT NULL AUTO_INCREMENT, | ||||
|   `name` varchar(255) NOT NULL DEFAULT '', | ||||
|  | @ -422,14 +569,14 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/blantyre',120); | |||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/brazzaville',60); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bujumbura',120); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/cairo',120); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',60); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',0); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ceuta',120); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/conakry',0); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dakar',0); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dar_es_salaam',180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/djibouti',180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/douala',60); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',60); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',0); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/freetown',0); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/gaborone',120); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/harare',120); | ||||
|  | @ -603,7 +750,7 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rio_branco',-300); | |||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rosario',-180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santarem',-180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santa_isabel',-420); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-240); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santo_domingo',-240); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sao_paulo',-180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/scoresbysund',0); | ||||
|  | @ -788,8 +935,8 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/pacific',-420); | |||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/saskatchewan',-360); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/yukon',-420); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cet',120); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-180); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-300); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-240); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-360); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cst6cdt',-300); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cuba',-240); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eet',180); | ||||
|  | @ -936,7 +1083,7 @@ INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/auckland',720); | |||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/bougainville',660); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chatham',765); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chuuk',600); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-300); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-360); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/efate',660); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/enderbury',780); | ||||
| INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fakaofo',780); | ||||
|  |  | |||
							
								
								
									
										13
									
								
								setup/sql/upgrade-00029.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								setup/sql/upgrade-00029.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| # Header section | ||||
| # Define incrementing schema version number | ||||
| SET @schema_version = '29'; | ||||
| 
 | ||||
| # Rename column tracking_disabled | ||||
| ALTER TABLE `campaigns` ADD COLUMN `open_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL, ADD COLUMN `click_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL; | ||||
| UPDATE `campaigns` SET `open_tracking_disabled` = `tracking_disabled`, `click_tracking_disabled` = `tracking_disabled`; | ||||
| ALTER TABLE `campaigns` DROP COLUMN `tracking_disabled`; | ||||
| 
 | ||||
| # Footer section | ||||
| LOCK TABLES `settings` WRITE; | ||||
| INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version; | ||||
| UNLOCK TABLES; | ||||
|  | @ -2,8 +2,9 @@ | |||
|   "parser": "babel-eslint", | ||||
|   "rules": { | ||||
|       "strict": 0, | ||||
|       "no-invalid-this": 0, | ||||
|       "no-unused-expressions": 0 | ||||
|       "no-console": 0, | ||||
|       "comma-dangle": 0, | ||||
|       "arrow-body-style": 0 | ||||
|   }, | ||||
|   "env": { | ||||
|       "mocha": true | ||||
|  |  | |||
|  | @ -1,11 +1,9 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| require('./lib/exit-unless-test'); | ||||
| const { mocha, driver } = require('./lib/mocha-e2e'); | ||||
| const mocha = require('./lib/mocha-e2e').mocha; | ||||
| const path = require('path'); | ||||
| 
 | ||||
| global.USE_SHARED_DRIVER = true; | ||||
| 
 | ||||
| const only = 'only'; | ||||
| const skip = 'skip'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,15 +13,46 @@ module.exports = { | |||
|         } | ||||
|     }, | ||||
|     lists: { | ||||
|         one: { | ||||
|         l1: { | ||||
|             id: 1, | ||||
|             cid: 'Hkj1vCoJb', | ||||
|             publicSubscribe: 1, | ||||
|             unsubscriptionMode: 0 | ||||
|             unsubscriptionMode: 0, // (one-step, no form)
 | ||||
|         }, | ||||
|         l2: { | ||||
|             id: 2, | ||||
|             cid: 'SktV4HDZ-', | ||||
|             publicSubscribe: 1, | ||||
|             unsubscriptionMode: 1, // (one-step, with form)
 | ||||
|         }, | ||||
|         l3: { | ||||
|             id: 3, | ||||
|             cid: 'BkdvNBw-W', | ||||
|             publicSubscribe: 1, | ||||
|             unsubscriptionMode: 2, // (two-step, no form)
 | ||||
|         }, | ||||
|         l4: { | ||||
|             id: 4, | ||||
|             cid: 'rJMKVrDZ-', | ||||
|             publicSubscribe: 1, | ||||
|             unsubscriptionMode: 3, // (two-step, with form)
 | ||||
|         }, | ||||
|         l5: { | ||||
|             id: 5, | ||||
|             cid: 'SJgoNSw-W', | ||||
|             publicSubscribe: 1, | ||||
|             unsubscriptionMode: 4, // (manual unsubscribe)
 | ||||
|         }, | ||||
|         l6: { | ||||
|             id: 6, | ||||
|             cid: 'HyveEPvWW', | ||||
|             publicSubscribe: 0, | ||||
|             unsubscriptionMode: 0, // (one-step, no form)
 | ||||
|         } | ||||
|     }, | ||||
|     settings: { | ||||
|         'service-url' : 'http://localhost:' + config.www.port + '/', | ||||
|         'service-url': 'http://localhost:' + config.www.port + '/', | ||||
|         'admin-email': 'admin@example.com', | ||||
|         'default-homepage': 'https://mailtrain.org', | ||||
|         'smtp-hostname': config.testserver.host, | ||||
|         'smtp-port': config.testserver.port, | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ module.exports = (...extras) => page({ | |||
|         await this.waitUntilVisible(); | ||||
|     }, | ||||
| 
 | ||||
|     async ensureUrl(path) { | ||||
|     async ensureUrl() { | ||||
|         throw new Error('Unsupported method.'); | ||||
|     }, | ||||
|     } | ||||
| 
 | ||||
| }, ...extras); | ||||
|  |  | |||
|  | @ -1,217 +1,213 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const Mocha = require('mocha'); | ||||
| const color = Mocha.reporters.Base.color; | ||||
| const Semaphore = require('./semaphore'); | ||||
| const fs = require('fs-extra'); | ||||
| const config = require('./config'); | ||||
| const webdriver = require('selenium-webdriver'); | ||||
| 
 | ||||
| const driver = new webdriver.Builder() | ||||
|     .forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs') | ||||
|     .build(); | ||||
| 
 | ||||
| 
 | ||||
| const failHandlerRunning = new Semaphore(); | ||||
| 
 | ||||
| 
 | ||||
| function UseCaseReporter(runner) { | ||||
|     Mocha.reporters.Base.call(this, runner); | ||||
| 
 | ||||
|     const self = this; | ||||
|     let indents = 0; | ||||
| 
 | ||||
|     function indent () { | ||||
|         return Array(indents).join('  '); | ||||
|     } | ||||
| 
 | ||||
|     runner.on('start', function () { | ||||
|         console.log(); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('suite', suite => { | ||||
|         ++indents; | ||||
|         console.log(color('suite', '%s%s'), indent(), suite.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('suite end', () => { | ||||
|         --indents; | ||||
|         if (indents === 1) { | ||||
|             console.log(); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('use-case', useCase => { | ||||
|         ++indents; | ||||
|         console.log(); | ||||
|         console.log(color('suite', '%sUse case: %s'), indent(), useCase.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('use-case end', () => { | ||||
|         --indents; | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('steps', useCase => { | ||||
|         ++indents; | ||||
|         console.log(color('pass', '%s%s'), indent(), useCase.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('steps end', () => { | ||||
|         --indents; | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('step pass', step => { | ||||
|         console.log(indent() + color('checkmark', '  ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('step fail', step => { | ||||
|         console.log(indent() + color('fail', '  %s'), step.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('pending', test => { | ||||
|         const fmt = indent() + color('pending', '  - %s'); | ||||
|         console.log(fmt, test.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('pass', test => { | ||||
|         let fmt; | ||||
|         if (test.speed === 'fast') { | ||||
|             fmt = indent() + | ||||
|                 color('checkmark', '  ' + Mocha.reporters.Base.symbols.ok) + | ||||
|                 color('pass', ' %s'); | ||||
|             console.log(fmt, test.title); | ||||
|         } else { | ||||
|             fmt = indent() + | ||||
|                 color('checkmark', '  ' + Mocha.reporters.Base.symbols.ok) + | ||||
|                 color('pass', ' %s') + | ||||
|                 color(test.speed, ' (%dms)'); | ||||
|             console.log(fmt, test.title, test.duration); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('fail', (test, err) => { | ||||
|         failHandlerRunning.enter(); | ||||
|         (async () => { | ||||
|             const currentUrl = await driver.getCurrentUrl(); | ||||
|             const info = `URL: ${currentUrl}`; | ||||
|             await fs.writeFile('last-failed-e2e-test.info', info); | ||||
|             await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource()); | ||||
|             await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64')); | ||||
|             failHandlerRunning.exit(); | ||||
|         })(); | ||||
| 
 | ||||
|         console.log(indent() + color('fail', '  %s'), test.title); | ||||
|         console.log(); | ||||
|         console.log(err); | ||||
|         console.log(); | ||||
|         console.log(`Snaphot of and info about the current page are in last-failed-e2e-test.*`); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('end', () => { | ||||
|         const stats = self.stats; | ||||
|         let fmt; | ||||
| 
 | ||||
|         console.log(); | ||||
| 
 | ||||
|         // passes
 | ||||
|         fmt = color('bright pass', ' ') + color('green', ' %d passing'); | ||||
|         console.log(fmt, stats.passes); | ||||
| 
 | ||||
|         // pending
 | ||||
|         if (stats.pending) { | ||||
|             fmt = color('pending', ' ') + color('pending', ' %d pending'); | ||||
|             console.log(fmt, stats.pending); | ||||
|         } | ||||
| 
 | ||||
|         // failures
 | ||||
|         if (stats.failures) { | ||||
|             fmt = color('fail', '  %d failing'); | ||||
|             console.log(fmt, stats.failures); | ||||
|         } | ||||
| 
 | ||||
|         console.log(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| const mocha = new Mocha() | ||||
|     .timeout(120000) | ||||
|     .reporter(UseCaseReporter) | ||||
|     .ui('tdd'); | ||||
| 
 | ||||
| mocha._originalRun = mocha.run; | ||||
| 
 | ||||
| 
 | ||||
| let runner; | ||||
| mocha.run = fn => { | ||||
|     runner = mocha._originalRun(async () => { | ||||
|         await failHandlerRunning.waitForEmpty(); | ||||
|         await driver.quit(); | ||||
| 
 | ||||
|         fn(); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| async function useCaseExec(name, asyncFn) { | ||||
|     runner.emit('use-case', {title: name}); | ||||
| 
 | ||||
|     try { | ||||
|         await asyncFn(); | ||||
|         runner.emit('use-case end'); | ||||
|     } catch (err) { | ||||
|         runner.emit('use-case end'); | ||||
|         throw err; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function useCase(name, asyncFn) { | ||||
|     if (asyncFn) { | ||||
|         return test('Use case: ' + name, () => useCaseExec(name, asyncFn)); | ||||
|     } else { | ||||
|         // Pending test
 | ||||
|         return test('Use case: ' + name); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| useCase.only = (name, asyncFn) => { | ||||
|     return test.only('Use case: ' + name, () => useCaseExec(name, asyncFn)); | ||||
| }; | ||||
| 
 | ||||
| useCase.skip = (name, asyncFn) => { | ||||
|     return test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn)); | ||||
| }; | ||||
| 
 | ||||
| async function step(name, asyncFn) { | ||||
|     try { | ||||
|         await asyncFn(); | ||||
|         runner.emit('step pass', {title: name}); | ||||
|     } catch (err) { | ||||
|         runner.emit('step fail', {title: name}); | ||||
|         throw err; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function steps(name, asyncFn) { | ||||
|     try { | ||||
|         runner.emit('steps', {title: name}); | ||||
|         await asyncFn(); | ||||
|         runner.emit('steps end'); | ||||
|     } catch (err) { | ||||
|         runner.emit('step end'); | ||||
|         throw err; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function precondition(preConditionName, useCaseName, asyncFn) { | ||||
|     await steps(`Including use case "${useCaseName}" to satisfy precondition "${preConditionName}"`, asyncFn); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     mocha, | ||||
|     useCase, | ||||
|     step, | ||||
|     steps, | ||||
|     precondition, | ||||
|     driver | ||||
| }; | ||||
| 'use strict'; | ||||
| 
 | ||||
| /* eslint-disable no-console */ | ||||
| 
 | ||||
| const Mocha = require('mocha'); | ||||
| const color = Mocha.reporters.Base.color; | ||||
| const WorkerCounter = require('./worker-counter'); | ||||
| const fs = require('fs-extra'); | ||||
| const config = require('./config'); | ||||
| const webdriver = require('selenium-webdriver'); | ||||
| 
 | ||||
| const driver = new webdriver.Builder() | ||||
|     .forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs') | ||||
|     .build(); | ||||
| 
 | ||||
| const failHandlerRunning = new WorkerCounter(); | ||||
| 
 | ||||
| function UseCaseReporter(runner) { | ||||
|     Mocha.reporters.Base.call(this, runner); | ||||
| 
 | ||||
|     const self = this; | ||||
|     let indents = 0; | ||||
| 
 | ||||
|     function indent () { | ||||
|         return Array(indents).join('  '); | ||||
|     } | ||||
| 
 | ||||
|     runner.on('start', () => { | ||||
|         console.log(); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('suite', suite => { | ||||
|         ++indents; | ||||
|         console.log(color('suite', '%s%s'), indent(), suite.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('suite end', () => { | ||||
|         --indents; | ||||
|         if (indents === 1) { | ||||
|             console.log(); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('use-case', useCase => { | ||||
|         ++indents; | ||||
|         console.log(); | ||||
|         console.log(color('suite', '%sUse case: %s'), indent(), useCase.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('use-case end', () => { | ||||
|         --indents; | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('steps', useCase => { | ||||
|         ++indents; | ||||
|         console.log(color('pass', '%s%s'), indent(), useCase.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('steps end', () => { | ||||
|         --indents; | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('step pass', step => { | ||||
|         console.log(indent() + color('checkmark', '  ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('step fail', step => { | ||||
|         console.log(indent() + color('fail', '  %s'), step.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('pending', test => { | ||||
|         const fmt = indent() + color('pending', '  - %s'); | ||||
|         console.log(fmt, test.title); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('pass', test => { | ||||
|         let fmt; | ||||
|         if (test.speed === 'fast') { | ||||
|             fmt = indent() + | ||||
|                 color('checkmark', '  ' + Mocha.reporters.Base.symbols.ok) + | ||||
|                 color('pass', ' %s'); | ||||
|             console.log(fmt, test.title); | ||||
|         } else { | ||||
|             fmt = indent() + | ||||
|                 color('checkmark', '  ' + Mocha.reporters.Base.symbols.ok) + | ||||
|                 color('pass', ' %s') + | ||||
|                 color(test.speed, ' (%dms)'); | ||||
|             console.log(fmt, test.title, test.duration); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('fail', (test, err) => { | ||||
|         failHandlerRunning.enter(); | ||||
|         (async () => { | ||||
|             const currentUrl = await driver.getCurrentUrl(); | ||||
|             const info = `URL: ${currentUrl}`; | ||||
|             await fs.writeFile('last-failed-e2e-test.info', info); | ||||
|             await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource()); | ||||
|             await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64')); | ||||
|             failHandlerRunning.exit(); | ||||
|         })(); | ||||
| 
 | ||||
|         console.log(indent() + color('fail', '  %s'), test.title); | ||||
|         console.log(); | ||||
|         console.log(err); | ||||
|         console.log(); | ||||
|         console.log('Snaphot of and info about the current page are in last-failed-e2e-test.*'); | ||||
|     }); | ||||
| 
 | ||||
|     runner.on('end', () => { | ||||
|         const stats = self.stats; | ||||
|         let fmt; | ||||
| 
 | ||||
|         console.log(); | ||||
| 
 | ||||
|         // passes
 | ||||
|         fmt = color('bright pass', ' ') + color('green', ' %d passing'); | ||||
|         console.log(fmt, stats.passes); | ||||
| 
 | ||||
|         // pending
 | ||||
|         if (stats.pending) { | ||||
|             fmt = color('pending', ' ') + color('pending', ' %d pending'); | ||||
|             console.log(fmt, stats.pending); | ||||
|         } | ||||
| 
 | ||||
|         // failures
 | ||||
|         if (stats.failures) { | ||||
|             fmt = color('fail', '  %d failing'); | ||||
|             console.log(fmt, stats.failures); | ||||
|         } | ||||
| 
 | ||||
|         console.log(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| const mocha = new Mocha() | ||||
|     .timeout(120000) | ||||
|     .reporter(UseCaseReporter) | ||||
|     .ui('tdd'); | ||||
| 
 | ||||
| mocha._originalRun = mocha.run; | ||||
| 
 | ||||
| 
 | ||||
| let runner; | ||||
| mocha.run = fn => { | ||||
|     runner = mocha._originalRun(async () => { | ||||
|         await failHandlerRunning.waitForEmpty(); | ||||
|         await driver.quit(); | ||||
| 
 | ||||
|         fn(); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| async function useCaseExec(name, asyncFn) { | ||||
|     runner.emit('use-case', {title: name}); | ||||
| 
 | ||||
|     try { | ||||
|         await asyncFn(); | ||||
|         runner.emit('use-case end'); | ||||
|     } catch (err) { | ||||
|         runner.emit('use-case end'); | ||||
|         throw err; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function useCase(name, asyncFn) { | ||||
|     if (asyncFn) { | ||||
|         return test('Use case: ' + name, () => useCaseExec(name, asyncFn)); | ||||
|     } else { | ||||
|         // Pending test
 | ||||
|         return test('Use case: ' + name); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| useCase.only = (name, asyncFn) => test.only('Use case: ' + name, () => useCaseExec(name, asyncFn)); | ||||
| 
 | ||||
| useCase.skip = (name, asyncFn) => test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn)); | ||||
| 
 | ||||
| async function step(name, asyncFn) { | ||||
|     try { | ||||
|         await asyncFn(); | ||||
|         runner.emit('step pass', {title: name}); | ||||
|     } catch (err) { | ||||
|         runner.emit('step fail', {title: name}); | ||||
|         throw err; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function steps(name, asyncFn) { | ||||
|     try { | ||||
|         runner.emit('steps', {title: name}); | ||||
|         await asyncFn(); | ||||
|         runner.emit('steps end'); | ||||
|     } catch (err) { | ||||
|         runner.emit('step end'); | ||||
|         throw err; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| async function precondition(preConditionName, useCaseName, asyncFn) { | ||||
|     await steps(`Including use case "${useCaseName}" to satisfy precondition "${preConditionName}"`, asyncFn); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     mocha, | ||||
|     useCase, | ||||
|     step, | ||||
|     steps, | ||||
|     precondition, | ||||
|     driver | ||||
| }; | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const config = require('./config'); | ||||
| const webdriver = require('selenium-webdriver'); | ||||
| const By = webdriver.By; | ||||
| const until = webdriver.until; | ||||
|  | @ -36,6 +35,10 @@ module.exports = (...extras) => Object.assign({ | |||
|     async waitUntilVisible(selector) { | ||||
|         await driver.wait(until.elementLocated(By.css('body')), waitTimeout); | ||||
| 
 | ||||
|         if (selector) { | ||||
|             await driver.wait(until.elementLocated(By.css(selector)), waitTimeout); | ||||
|         } | ||||
|          | ||||
|         for (const elem of (this.elementsToWaitFor || [])) { | ||||
|             const sel = this.elements[elem]; | ||||
|             if (!sel) { | ||||
|  | @ -45,9 +48,7 @@ module.exports = (...extras) => Object.assign({ | |||
|         } | ||||
| 
 | ||||
|         for (const text of (this.textsToWaitFor || [])) { | ||||
|             await driver.wait(new webdriver.Condition(`for text "${text}"`, async (driver) => { | ||||
|                 return await this.containsText(text); | ||||
|             }), waitTimeout); | ||||
|             await driver.wait(new webdriver.Condition(`for text "${text}"`, async () => await this.containsText(text)), waitTimeout); | ||||
|         } | ||||
| 
 | ||||
|         if (this.url) { | ||||
|  | @ -58,7 +59,7 @@ module.exports = (...extras) => Object.assign({ | |||
|     }, | ||||
| 
 | ||||
|     async waitUntilVisibleAfterRefresh(selector) { | ||||
|         await driver.wait(new webdriver.Condition('for refresh', async (driver) => { | ||||
|         await driver.wait(new webdriver.Condition('for refresh', async driver => { | ||||
|             const val = await driver.executeScript('return document.mailTrainRefreshAcknowledged;'); | ||||
|             return !val; | ||||
|         }), waitTimeout); | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ module.exports = (...extras) => page({ | |||
|             path = pathOrParams; | ||||
|         } else { | ||||
|             const urlPattern = new UrlPattern(this.requestUrl || this.url); | ||||
|             path = urlPattern.stringify(pathOrParams) | ||||
|             path = urlPattern.stringify(pathOrParams); | ||||
|         } | ||||
| 
 | ||||
|         const parsedUrl = url.parse(path); | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ class WorkerCounter { | |||
|         const self = this; | ||||
| 
 | ||||
|         function wait(resolve) { | ||||
|             if (self.counter == 0) { | ||||
|             if (self.counter === 0) { | ||||
|                 resolve(); | ||||
|             } else { | ||||
|                 setTimeout(wait, 500, resolve); | ||||
|  | @ -28,8 +28,8 @@ class WorkerCounter { | |||
| 
 | ||||
|         return new Promise(resolve => { | ||||
|             setTimeout(wait, 500, resolve); | ||||
|         }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports = WorkerCounter; | ||||
| module.exports = WorkerCounter; | ||||
|  |  | |||
|  | @ -19,6 +19,11 @@ module.exports = list => ({ | |||
|         } | ||||
|     }), | ||||
| 
 | ||||
|     webSubscribeNonPublic: web({ | ||||
|         url: `/subscription/${list.cid}`, | ||||
|         textsToWaitFor: ['The list does not allow public subscriptions'], | ||||
|     }), | ||||
| 
 | ||||
|     webConfirmSubscriptionNotice: web({ | ||||
|         url: `/subscription/${list.cid}/confirm-subscription-notice`, | ||||
|         textsToWaitFor: ['We need to confirm your email address'] | ||||
|  | @ -88,7 +93,7 @@ module.exports = list => ({ | |||
|             form: `form[action="/subscription/${list.cid}/manage-address"]`, | ||||
|             emailInput: '#main-form input[name="email"]', | ||||
|             emailNewInput: '#main-form input[name="email-new"]', | ||||
|             submitButton: 'a[href="#submit"]', | ||||
|             submitButton: 'a[href="#submit"]' | ||||
|         } | ||||
|     }), | ||||
| 
 | ||||
|  | @ -102,12 +107,12 @@ module.exports = list => ({ | |||
| 
 | ||||
|     webUpdatedNotice: web({ | ||||
|         url: `/subscription/${list.cid}/updated-notice`, | ||||
|         textsToWaitFor: ['Profile Updated'], | ||||
|         textsToWaitFor: ['Profile Updated'] | ||||
|     }), | ||||
| 
 | ||||
|     webUnsubscribedNotice: web({ | ||||
|         url: `/subscription/${list.cid}/unsubscribed-notice`, | ||||
|         textsToWaitFor: ['Unsubscribe Successful'], | ||||
|         textsToWaitFor: ['Unsubscribe Successful'] | ||||
|     }), | ||||
| 
 | ||||
|     mailUnsubscriptionConfirmed: mail({ | ||||
|  | @ -118,15 +123,34 @@ module.exports = list => ({ | |||
|         } | ||||
|     }), | ||||
| 
 | ||||
|     /* | ||||
|     webUnsubscribe: web({ // FIXME
 | ||||
|     webUnsubscribe: web({ | ||||
|         elementsToWaitFor: ['submitButton'], | ||||
|         textsToWaitFor: ['Unsubscribe'], | ||||
|         elements: { | ||||
|             submitButton: 'a[href="#submit"]' | ||||
|         } | ||||
|     }), | ||||
| 
 | ||||
| */ | ||||
|     webConfirmUnsubscriptionNotice: web({ | ||||
|         url: `/subscription/${list.cid}/confirm-unsubscription-notice`, | ||||
|         textsToWaitFor: ['We need to confirm your email address'] | ||||
|     }), | ||||
| 
 | ||||
|     mailConfirmUnsubscription: mail({ | ||||
|         elementsToWaitFor: ['confirmLink'], | ||||
|         textsToWaitFor: ['Please Confirm Unsubscription'], | ||||
|         elements: { | ||||
|             confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/unsubscribe/"]` | ||||
|         } | ||||
|     }), | ||||
| 
 | ||||
|     webManualUnsubscribeNotice: web({ | ||||
|         url: `/subscription/${list.cid}/manual-unsubscribe-notice`, | ||||
|         elementsToWaitFor: ['contactLink'], | ||||
|         textsToWaitFor: ['Online Unsubscription Is Not Possible', config.settings['admin-email']], | ||||
|         elements: { | ||||
|             contactLink: `a[href^="mailto:${config.settings['admin-email']}"]` | ||||
|         } | ||||
|     }), | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,8 +22,8 @@ module.exports = { | |||
|         url: '/users/account', | ||||
|         elementsToWaitFor: ['form'], | ||||
|         elements: { | ||||
|             form: `form[action="/users/account"]`, | ||||
|             form: 'form[action="/users/account"]', | ||||
|             emailInput: 'form[action="/users/account"] input[name="email"]' | ||||
|         } | ||||
|     }), | ||||
|     }) | ||||
| }; | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| /* eslint-disable prefer-arrow-callback */ | ||||
| 
 | ||||
| const config = require('../lib/config'); | ||||
| const { useCase, step, driver } = require('../lib/mocha-e2e'); | ||||
| const expect = require('chai').expect; | ||||
|  | @ -7,7 +9,7 @@ const expect = require('chai').expect; | |||
| const page = require('../page-objects/user'); | ||||
| const home = require('../page-objects/home'); | ||||
| 
 | ||||
| suite('Login use-cases', function() { | ||||
| suite('Login use-cases', () => { | ||||
|     before(() => driver.manage().deleteAllCookies()); | ||||
| 
 | ||||
|     test('User can access home page', async () => { | ||||
|  |  | |||
|  | @ -1,17 +1,24 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| /* eslint-disable prefer-arrow-callback */ | ||||
| 
 | ||||
| const config = require('../lib/config'); | ||||
| const { useCase, step, precondition, driver } = require('../lib/mocha-e2e'); | ||||
| const shortid = require('shortid'); | ||||
| const expect = require('chai').expect; | ||||
| const createPage = require('../page-objects/subscription'); | ||||
| 
 | ||||
| const page = require('../page-objects/subscription')(config.lists.one); | ||||
| function getPage(listConf) { | ||||
|     return createPage(listConf); | ||||
| } | ||||
| 
 | ||||
| function generateEmail() { | ||||
|     return 'keep.' + shortid.generate() + '@mailtrain.org'; | ||||
| } | ||||
| 
 | ||||
| async function subscribe(subscription) { | ||||
| async function subscribe(listConf, subscription) { | ||||
|     const page = getPage(listConf); | ||||
| 
 | ||||
|     await step('User navigates to list subscription page.', async () => { | ||||
|         await page.webSubscribe.navigate(); | ||||
|     }); | ||||
|  | @ -60,23 +67,25 @@ async function subscribe(subscription) { | |||
|     return subscription; | ||||
| } | ||||
| 
 | ||||
| async function subscriptionExistsPrecondition(subscription) { | ||||
| async function subscriptionExistsPrecondition(listConf, subscription) { | ||||
|     await precondition('Subscription exists', 'Subscription to a public list (main scenario)', async () => { | ||||
|         await subscribe(subscription); | ||||
|         await subscribe(listConf, subscription); | ||||
|     }); | ||||
|     return subscription; | ||||
| } | ||||
| 
 | ||||
| suite('Subscription use-cases', function() { | ||||
| suite('Subscription use-cases', () => { | ||||
|     before(() => driver.manage().deleteAllCookies()); | ||||
| 
 | ||||
|     useCase('Subscription to a public list (main scenario)', async () => { | ||||
|         await subscribe({ | ||||
|         await subscribe(config.lists.l1, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Subscription to a public list (invalid email)', async () => { | ||||
|         const page = getPage(config.lists.l1); | ||||
| 
 | ||||
|         await step('User navigates to list subscribe page', async () => { | ||||
|             await page.webSubscribe.navigate(); | ||||
|         }); | ||||
|  | @ -93,7 +102,9 @@ suite('Subscription use-cases', function() { | |||
|     }); | ||||
| 
 | ||||
|     useCase('Subscription to a public list (email already registered)', async () => { | ||||
|         const subscription = await subscriptionExistsPrecondition({ | ||||
|         const page = getPage(config.lists.l1); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l1, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
| 
 | ||||
|  | @ -116,10 +127,18 @@ suite('Subscription use-cases', function() { | |||
| 
 | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Subscription to a non-public list'); | ||||
|     useCase('Subscription to a non-public list', async () => { | ||||
|         const page = getPage(config.lists.l6); | ||||
| 
 | ||||
|         await step('User navigates to list subscription page and sees message that this list does not allow public subscriptions.', async () => { | ||||
|             await page.webSubscribeNonPublic.navigate(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Change profile info', async () => { | ||||
|         const subscription = await subscriptionExistsPrecondition({ | ||||
|         const page = getPage(config.lists.l1); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l1, { | ||||
|             email: generateEmail(), | ||||
|             firstName: 'John', | ||||
|             lastName: 'Doe' | ||||
|  | @ -161,7 +180,9 @@ suite('Subscription use-cases', function() { | |||
|     }); | ||||
| 
 | ||||
|     useCase('Change email', async () => { | ||||
|         const subscription = await subscriptionExistsPrecondition({ | ||||
|         const page = getPage(config.lists.l1); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l1, { | ||||
|             email: generateEmail(), | ||||
|             firstName: 'John', | ||||
|             lastName: 'Doe' | ||||
|  | @ -219,7 +240,9 @@ suite('Subscription use-cases', function() { | |||
|     }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #1 (one-step, no form).', async () => { | ||||
|         const subscription = await subscriptionExistsPrecondition({ | ||||
|         const page = getPage(config.lists.l1); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l1, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
| 
 | ||||
|  | @ -236,13 +259,181 @@ suite('Subscription use-cases', function() { | |||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #2 (one-step, with form).'); | ||||
|     useCase('Unsubscription from list #2 (one-step, with form).', async () => { | ||||
|         const page = getPage(config.lists.l2); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #3 (two-step, no form).'); | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l2, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #4 (two-step, with form).'); | ||||
|         await step('User clicks the unsubscribe button.', async () => { | ||||
|             await page.mailSubscriptionConfirmed.click('unsubscribeLink'); | ||||
|         }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #5 (manual unsubscribe).'); | ||||
|         await step('Systems shows a form to unsubscribe.', async () => { | ||||
|             await page.webUnsubscribe.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User confirms unsubscribe and clicks the unsubscribe button.', async () => { | ||||
|             await page.webUnsubscribe.submit(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that confirms unsubscription.', async () => { | ||||
|             await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email that confirms unsubscription.', async () => { | ||||
|             await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #3 (two-step, no form).', async () => { | ||||
|         const page = getPage(config.lists.l3); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l3, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the unsubscribe button.', async () => { | ||||
|             await page.mailSubscriptionConfirmed.click('unsubscribeLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that further instructions are in the email.', async () => { | ||||
|             await page.webConfirmUnsubscriptionNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email with a link to confirm unsubscription.', async () => { | ||||
|             await page.mailConfirmUnsubscription.fetchMail(subscription.email); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the confirm unsubscribe button in the email.', async () => { | ||||
|             await page.mailConfirmUnsubscription.click('confirmLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that confirms unsubscription.', async () => { | ||||
|             await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email that confirms unsubscription.', async () => { | ||||
|             await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #4 (two-step, with form).', async () => { | ||||
|         const page = getPage(config.lists.l4); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l4, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the unsubscribe button.', async () => { | ||||
|             await page.mailSubscriptionConfirmed.click('unsubscribeLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('Systems shows a form to unsubscribe.', async () => { | ||||
|             await page.webUnsubscribe.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User confirms unsubscribe and clicks the unsubscribe button.', async () => { | ||||
|             await page.webUnsubscribe.submit(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that further instructions are in the email.', async () => { | ||||
|             await page.webConfirmUnsubscriptionNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email with a link to confirm unsubscription.', async () => { | ||||
|             await page.mailConfirmUnsubscription.fetchMail(subscription.email); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the confirm unsubscribe button in the email.', async () => { | ||||
|             await page.mailConfirmUnsubscription.click('confirmLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that confirms unsubscription.', async () => { | ||||
|             await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email that confirms unsubscription.', async () => { | ||||
|             await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Unsubscription from list #5 (manual unsubscribe).', async () => { | ||||
|         const page = getPage(config.lists.l5); | ||||
| 
 | ||||
|         await subscriptionExistsPrecondition(config.lists.l5, { | ||||
|             email: generateEmail() | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the unsubscribe button.', async () => { | ||||
|             await page.mailSubscriptionConfirmed.click('unsubscribeLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('Systems shows a notice that online unsubscription is not possible.', async () => { | ||||
|             await page.webManualUnsubscribeNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Resubscription.', async () => { | ||||
|         const page = getPage(config.lists.l1); | ||||
| 
 | ||||
|         const subscription = await subscriptionExistsPrecondition(config.lists.l1, { | ||||
|             email: generateEmail(), | ||||
|             firstName: 'John', | ||||
|             lastName: 'Doe' | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the unsubscribe button.', async () => { | ||||
|             await page.mailSubscriptionConfirmed.click('unsubscribeLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that confirms unsubscription.', async () => { | ||||
|             await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email that confirms unsubscription.', async () => { | ||||
|             await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks the resubscribe button.', async () => { | ||||
|             await page.mailUnsubscriptionConfirmed.click('resubscribeLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('Systems shows the subscription form. The form contains data entered during initial subscription.', async () => { | ||||
|             await page.webSubscribe.waitUntilVisibleAfterRefresh(); | ||||
|             expect(await page.webSubscribe.getValue('emailInput')).to.equal(subscription.email); | ||||
|             expect(await page.webSubscribe.getValue('firstNameInput')).to.equal(subscription.firstName); | ||||
|             expect(await page.webSubscribe.getValue('lastNameInput')).to.equal(subscription.lastName); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User submits the subscription form.', async () => { | ||||
|             await page.webSubscribe.submit(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that further instructions are in the email.', async () => { | ||||
|             await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email with a link to confirm the subscription.', async () => { | ||||
|             await page.mailConfirmSubscription.fetchMail(subscription.email); | ||||
|         }); | ||||
| 
 | ||||
|         await step('User clicks confirm subscription in the email', async () => { | ||||
|             await page.mailConfirmSubscription.click('confirmLink'); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System shows a notice that subscription has been confirmed.', async () => { | ||||
|             await page.webSubscribedNotice.waitUntilVisibleAfterRefresh(); | ||||
|         }); | ||||
| 
 | ||||
|         await step('System sends an email with subscription confirmation. The manage and unsubscribe links are identical with the initial subscription.', async () => { | ||||
|             await page.mailSubscriptionConfirmed.fetchMail(subscription.email); | ||||
|             const unsubscribeLink = await page.mailSubscriptionConfirmed.getHref('unsubscribeLink'); | ||||
|             const manageLink = await page.mailSubscriptionConfirmed.getHref('manageLink'); | ||||
|             expect(subscription.unsubscribeLink).to.equal(unsubscribeLink); | ||||
|             expect(subscription.manageLink).to.equal(manageLink); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     useCase('Resubscription.'); // This one is supposed to check that values pre-filled in resubscription (i.e. the re-subscribe link in unsubscription confirmation) are the same as the ones used before.
 | ||||
| }); | ||||
|  |  | |||
|  | @ -84,7 +84,15 @@ | |||
|     <div class="col-sm-offset-2"> | ||||
|         <div class="checkbox"> | ||||
|             <label> | ||||
|                 <input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}} | ||||
|                 <input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="col-sm-offset-2"> | ||||
|         <div class="checkbox"> | ||||
|             <label> | ||||
|                 <input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -104,7 +104,15 @@ | |||
|     <div class="col-sm-offset-2"> | ||||
|         <div class="checkbox"> | ||||
|             <label> | ||||
|                 <input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}} | ||||
|                 <input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="col-sm-offset-2"> | ||||
|         <div class="checkbox"> | ||||
|             <label> | ||||
|                 <input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -110,7 +110,15 @@ | |||
|     <div class="col-sm-offset-2"> | ||||
|         <div class="checkbox"> | ||||
|             <label> | ||||
|                 <input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}} | ||||
|                 <input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="col-sm-offset-2"> | ||||
|         <div class="checkbox"> | ||||
|             <label> | ||||
|                 <input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -111,7 +111,15 @@ | |||
|         <div class="col-sm-offset-2"> | ||||
|             <div class="checkbox"> | ||||
|                 <label> | ||||
|                     <input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}} | ||||
|                     <input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}} | ||||
|                 </label> | ||||
|             </div> | ||||
|         </div> | ||||
|          | ||||
|         <div class="col-sm-offset-2"> | ||||
|             <div class="checkbox"> | ||||
|                 <label> | ||||
|                     <input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}} | ||||
|                 </label> | ||||
|             </div> | ||||
|         </div> | ||||
|  |  | |||
|  | @ -103,7 +103,15 @@ | |||
|                     <div class="col-sm-offset-2"> | ||||
|                         <div class="checkbox"> | ||||
|                             <label> | ||||
|                                 <input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}} | ||||
|                                 <input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                      | ||||
|                     <div class="col-sm-offset-2"> | ||||
|                         <div class="checkbox"> | ||||
|                             <label> | ||||
|                                 <input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  |  | |||
|  | @ -121,7 +121,15 @@ | |||
|                     <div class="col-sm-offset-2"> | ||||
|                         <div class="checkbox"> | ||||
|                             <label> | ||||
|                                 <input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked/opened tracking{{/translate}} | ||||
|                                 <input type="checkbox" name="open-tracking-disabled" value="1" {{#if openTrackingDisabled}} checked {{/if}}> {{#translate}}Disable opened tracking{{/translate}} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                      | ||||
|                     <div class="col-sm-offset-2"> | ||||
|                         <div class="checkbox"> | ||||
|                             <label> | ||||
|                                 <input type="checkbox" name="click-tracking-disabled" value="1" {{#if clickTrackingDisabled}} checked {{/if}}> {{#translate}}Disable clicked tracking{{/translate}} | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  |  | |||
|  | @ -164,7 +164,7 @@ | |||
|                     </div> | ||||
|                 </dd> | ||||
| 
 | ||||
|                 {{#unless trackingDisabled}} | ||||
|                 {{#unless openTrackingDisabled}} | ||||
| 
 | ||||
|                     <dt>{{#translate}}Opened{{/translate}} <a href="/campaigns/opened/{{id}}" title="{{#translate}}List subscribers who opened this message{{/translate}}"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt> | ||||
|                     <dd> | ||||
|  | @ -174,7 +174,8 @@ | |||
|                             </div> | ||||
|                         </div> | ||||
|                     </dd> | ||||
| 
 | ||||
|                 {{/unless}} | ||||
|                 {{#unless clickTrackingDisabled}} | ||||
|                     <dt>{{#translate}}Clicked{{/translate}} <a href="/campaigns/clicked/{{id}}/all" title="{{#translate}}List subscribers who clicked on a link{{/translate}}"> <span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt> | ||||
|                     <dd> | ||||
|                         <div class="progress"> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue