' ).text( object.text );
-
- if ( 'undefined' !== typeof object.icon && object.icon ) {
- $result.prepend( $( '

' ) );
-
- // Add more info to the container
- $result.attr( 'title', object.tooltip );
- }
-
- // Add more info to the container
- if ( 'undefined' !== typeof object.tooltip ) {
- $result.attr( 'title', object.tooltip );
- } else if ( 'undefined' !== typeof object.user_count ) {
- $result.attr( 'title', object.user_count );
- }
-
- return $result;
- },
- templateSelection( object ) {
- const $result = $( '
' ).text( object.text );
-
- if ( $.isNumeric( object.id ) && object.text.indexOf( 'icon-users' ) < 0 ) {
- $result.append( $( '' ) );
- }
-
- return $result;
- },
- allowClear: true,
- placeholder: $input_user.data( 'placeholder' ),
- },
- ).on(
- 'change', function() {
- const value = $( this ).select2( 'data' );
-
- $( this ).data( 'selected-id', value.id );
- $( this ).data( 'selected-text', value.text );
- },
- );
- },
- );
-
- $( 'select.select2-select.ip_address', $rowsWithSelect2 ).each(
- function( k, el ) {
- const $input_ip = $( el );
- let searchTerm = '';
-
- $input_ip.select2(
- {
- ajax: {
- type: 'POST',
- url: window.ajaxurl,
- dataType: 'json',
- quietMillis: 500,
- data( term ) {
- searchTerm = term.term;
- return {
- find: term,
- limit: 10,
- action: 'stream_get_ips',
- nonce: $input_ip.data( 'nonce' ),
- };
- },
- processResults( response ) {
- const answer = { results: [] };
- let ip_chunks = [];
-
- if ( true === response.success && undefined !== response.data ) {
- $.each(
- response.data, function( key, ip ) {
- answer.results.push(
- {
- id: ip,
- text: ip,
- },
- );
- },
- );
- }
-
- if ( undefined === searchTerm ) {
- return answer;
- }
-
- ip_chunks = searchTerm.match( /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ );
-
- if ( null === ip_chunks ) {
- return answer;
- }
-
- // remove whole match
- ip_chunks.shift();
-
- ip_chunks = $.grep(
- ip_chunks,
- function( chunk ) {
- const numeric = parseInt( chunk, 10 );
- return numeric <= 255 && numeric.toString() === chunk;
- },
- );
-
- if ( ip_chunks.length >= 4 ) {
- answer.results.push(
- {
- id: searchTerm,
- text: searchTerm,
- },
- );
- }
-
- return answer;
- },
- },
- allowClear: false,
- multiple: true,
- maximumSelectionSize: 1,
- placeholder: $input_ip.data( 'placeholder' ),
- tags: true,
- },
- );
- },
- ).on(
+/**
+ * Bind native exclude-rule controls for one or more rows.
+ *
+ * @param {Object} $rows jQuery collection of table rows.
+ */
+function initExcludeRows( $rows ) {
+ $( 'select.connector_or_context', $rows ).on(
'change', function() {
- $( this ).prev( '.select2-container' ).find( 'input.select2-input' ).blur();
- },
- );
-
- $( 'ul.select2-choices, ul.select2-choices li, input.select2-input', '.stream-exclude-list tr:not(.hidden) .ip_address' ).on(
- 'mousedown click focus', function() {
- const $container = $( this ).closest( '.select2-container' ),
- $input = $container.find( 'input.select2-input' ),
- value = $container.select2( 'data' );
-
- if ( value.length >= 1 ) {
- $input.blur();
- return false;
+ const row = $( this ).closest( 'tr' );
+ let connector = $( this ).val();
+ if ( connector && 0 < connector.indexOf( '-' ) ) {
+ const connector_split = connector.split( '-' );
+ connector = connector_split[ 0 ];
}
+ getActions( row, connector );
},
);
- $( '.exclude_rules_remove_rule_row', $rowsWithSelect2 ).on(
+ $( '.exclude_rules_remove_rule_row', $rows ).on(
'click', function( e ) {
const $thisRow = $( this ).closest( 'tr' );
@@ -294,18 +42,11 @@ const initSettingsSelect2 = function( $rowsWithSelect2 ) {
e.preventDefault();
},
);
-};
-
-initSettingsSelect2( $excludeRows );
+}
-$( 'select.select2-select.author_or_role', $excludeRows ).each(
- function() {
- const $option = $( '' ).val( $( this ).data( 'selected-id' ) );
- $( this ).append( $option ).trigger( 'change' );
- },
-);
+initExcludeRows( $excludeRows );
-$( 'select.select2-select.connector_or_context', $excludeRows ).each(
+$( 'select.connector_or_context', $excludeRows ).each(
function() {
const parts = [
$( this ).siblings( '.connector' ).val(),
@@ -325,7 +66,7 @@ $( '#exclude_rules_new_rule' ).on(
$newRow.removeAttr( 'class' );
$newRow.insertBefore( $placeholderRow );
- initSettingsSelect2( $newRow );
+ initExcludeRows( $newRow );
recalculate_rules_found();
recalculate_rules_selected();
},
@@ -341,7 +82,6 @@ $( '#exclude_rules_remove_rules' ).on(
} else {
$( ':input', selectedRows ).val( '' );
$( selectedRows ).not( ':first' ).remove();
- $( '.select2-select', selectedRows ).select2( 'val', '' );
}
$excludeList.find( 'input.cb-select' ).prop( 'checked', false );
@@ -358,31 +98,14 @@ $( '.stream-exclude-list' ).closest( 'form' ).submit(
$( this ).find( ':input' ).removeAttr( 'name' );
},
);
- $( '.stream-exclude-list tbody tr:not(.hidden) select.select2-select.connector_or_context', this ).each(
+ $( '.stream-exclude-list tbody tr:not(.hidden) select.connector_or_context', this ).each(
function() {
const parts = $( this ).val().split( '-' );
$( this ).siblings( '.connector' ).val( parts[ 0 ] );
- $( this ).siblings( '.context' ).val( parts.slice( 1 ).join( '-' ) );
+ $( this ).siblings( '.context' ).val( parts[ 1 ] );
$( this ).removeAttr( 'name' );
},
);
- $( '.stream-exclude-list tbody tr:not(.hidden) select.select2-select.ip_address', this ).each(
- function() {
- const firstSelected = $( 'option:selected', this ).first();
-
- // Ugly hack to ensure we always pass an empty value or the order of rows gets messed up.
- if ( ! firstSelected.length ) {
- $( this ).append( '' );
- }
-
- $( 'option:selected:not(:first)', this ).each(
- function() {
- firstSelected.attr( 'value', firstSelected.attr( 'value' ) + ',' + $( this ).attr( 'value' ) );
- $( this ).removeAttr( 'selected' );
- },
- );
- },
- );
},
);
@@ -395,13 +118,13 @@ $( 'table.stream-exclude-list' ).on(
);
function getActions( row, connector ) {
- const trigger_action = $( '.select2-select.action', row ),
+ const trigger_action = $( 'select.action', row ),
action_value = trigger_action.val();
trigger_action.empty();
trigger_action.prop( 'disabled', true );
- const placeholder = $( '', { value: '', text: '' } );
+ const placeholder = $( '', { value: '', text: trigger_action.data( 'placeholder' ) || '' } );
trigger_action.append( placeholder );
const data = {
diff --git a/src/js/admin.js b/src/js/admin.js
index d3fe817d5..f60530277 100644
--- a/src/js/admin.js
+++ b/src/js/admin.js
@@ -9,16 +9,7 @@ import $ from 'jquery';
*/
import '../css/admin.scss';
import getQueryVars from './utils/get-query-vars';
-
-// Shorter timeago strings for English locale
-if ( 'en' === window[ 'wp-stream-admin' ].locale && 'undefined' !== typeof $.timeago ) {
- $.timeago.settings.strings.seconds = 'seconds';
- $.timeago.settings.strings.minute = 'a minute';
- $.timeago.settings.strings.hour = 'an hour';
- $.timeago.settings.strings.hours = '%d hours';
- $.timeago.settings.strings.month = 'a month';
- $.timeago.settings.strings.year = 'a year';
-}
+import formatRelativeTime from './utils/relative-time';
$( 'li.toplevel_page_wp_stream ul li.wp-first-item.current' ).parent().parent().find( '.update-plugins' ).remove();
@@ -273,10 +264,13 @@ $( 'table.wp-list-table' ).on(
function( i, el ) {
const timeEl = $( el );
timeEl.removeClass( 'relative-time' );
- $( '
' )
- .prependTo( timeEl.parent().parent() )
- .find( 'time.timeago' )
- .timeago();
+ const text = formatRelativeTime( timeEl.attr( 'datetime' ) );
+ if ( text ) {
+ $( '
' )
+ .prependTo( timeEl.parent().parent() )
+ .find( 'time.timeago' )
+ .text( text );
+ }
},
);
},
diff --git a/src/js/alerts-list.js b/src/js/alerts-list.js
index 580f8f1da..09321a40a 100644
--- a/src/js/alerts-list.js
+++ b/src/js/alerts-list.js
@@ -13,6 +13,3 @@ $( '.inline-edit-col-left, .inline-edit-col-right, #major-publishing-actions', '
$( this ).remove();
},
);
-
-// This is done with JS instead of CSS to override the inline styles added by Select2's JS.
-$( '.select2-container', '.inline-edit-col' ).css( { width: '100%' } );
diff --git a/src/js/alerts.js b/src/js/alerts.js
index 13ad91774..90bef15e0 100644
--- a/src/js/alerts.js
+++ b/src/js/alerts.js
@@ -6,57 +6,12 @@ import $ from 'jquery';
let $post_row,
$edit_row;
-function setupSelectTwo( id ) {
+function bindTriggerSelects( id ) {
const $target = $( id );
- $target.find( '.select2-select.connector_or_context' ).each(
+ $target.find( 'select.connector_or_context' ).each(
function( k, el ) {
- $( el ).select2(
- {
- allowClear: true,
- placeholder: window[ 'wp-stream-alerts' ].anyContext,
- templateResult( item ) {
- if ( 'undefined' === typeof item.id ) {
- return item.text;
- }
- if ( -1 === item.id.indexOf( '-' ) ) {
- return $( '' + item.text + '' );
- }
- return $( '' + item.text + '' );
- },
- matcher( params, data ) {
- const match = $.extend( true, {}, data );
-
- if ( null === params.term || '' === $.trim( params.term ) ) {
- return match;
- }
-
- const term = params.term.toLowerCase();
-
- match.id = match.id.replace( 'blogs', 'sites' );
- if ( match.id.toLowerCase().indexOf( term ) >= 0 ) {
- return match;
- }
-
- if ( match.children ) {
- for ( let i = match.children.length - 1; i >= 0; i-- ) {
- const child = match.children[ i ];
-
- // Remove term from results if it doesn't match.
- if ( -1 === child.id.toLowerCase().indexOf( term ) ) {
- match.children.splice( i, 1 );
- }
- }
-
- if ( match.children.length > 0 ) {
- return match;
- }
- }
-
- return null;
- },
- },
- ).change(
- function() {
+ $( el ).on(
+ 'change', function() {
const value = $( this ).val();
if ( value ) {
const parts = value.split( '-' );
@@ -76,20 +31,6 @@ function setupSelectTwo( id ) {
$( el ).val( parts.join( '-' ) ).trigger( 'change' );
},
);
-
- $target.find( 'select.select2-select:not(.connector_or_context)' ).each(
- function() {
- const element_id_split = $( this ).attr( 'id' ).split( '_' );
- const select_name = element_id_split[ element_id_split.length - 1 ].charAt( 0 ).toUpperCase() +
- element_id_split[ element_id_split.length - 1 ].slice( 1 );
- $( this ).select2(
- {
- allowClear: true,
- placeholder: window[ 'wp-stream-alerts' ].any + ' ' + select_name,
- },
- );
- },
- );
}
const $alertSettingSelect = $( '#wp_stream_alert_type' );
@@ -134,7 +75,7 @@ function getActions( connector ) {
trigger_action.empty();
trigger_action.prop( 'disabled', true );
- const placeholder = $( '', { value: '', text: '' } );
+ const placeholder = $( '', { value: '', text: trigger_action.data( 'placeholder' ) || '' } );
trigger_action.append( placeholder );
const data = {
@@ -209,7 +150,7 @@ $( '#wpbody-content' ).on(
);
add_new_alert.on( 'click', '.button-primary.save', save_new_alert );
- setupSelectTwo( '#add-new-alert' );
+ bindTriggerSelects( '#add-new-alert' );
}
},
);
@@ -286,7 +227,7 @@ window.inlineEditPost.edit = function( id ) {
},
);
$edit_row.find( 'select[name="wp_stream_alert_status"] option[value="' + alert_status + '"]' ).attr( 'selected', 'selected' );
- setupSelectTwo( '#edit-' + post_id );
+ bindTriggerSelects( '#edit-' + post_id );
// Alert type handling
$( '#wp_stream_alert_type_form' ).hide();
diff --git a/src/js/live-updates.js b/src/js/live-updates.js
index 930cbc3cf..7d0d24863 100644
--- a/src/js/live-updates.js
+++ b/src/js/live-updates.js
@@ -101,7 +101,7 @@ $( document ).ready(
$( '.tablenav-pages .last-page' ).attr( 'href', data.last_page_link );
}
- // Allow others to hook in, ie: timeago
+ // Allow others to hook in (relative time on new rows).
$( list_sel ).parent().trigger( 'updated' );
// Regenerate alternating row classes
diff --git a/src/js/utils/relative-time.js b/src/js/utils/relative-time.js
new file mode 100644
index 000000000..96bed4de1
--- /dev/null
+++ b/src/js/utils/relative-time.js
@@ -0,0 +1,67 @@
+const DIVISIONS = [
+ { amount: 60, unit: 'second' },
+ { amount: 60, unit: 'minute' },
+ { amount: 24, unit: 'hour' },
+ { amount: 7, unit: 'day' },
+ { amount: 4.34524, unit: 'week' },
+ { amount: 12, unit: 'month' },
+ { amount: Number.POSITIVE_INFINITY, unit: 'year' },
+];
+
+const formatters = {};
+
+/**
+ * Locale for relative strings: the document language, then the admin script locale.
+ *
+ * @return {string|undefined} BCP 47 tag, or undefined for the runtime default.
+ */
+function resolveLocale() {
+ const admin = window[ 'wp-stream-admin' ];
+ return (
+ ( document.documentElement && document.documentElement.lang ) ||
+ ( admin && admin.locale ) ||
+ undefined
+ );
+}
+
+/**
+ * Memoized Intl.RelativeTimeFormat per locale (one per page, not one per row).
+ *
+ * @param {string|undefined} locale Locale tag.
+ * @return {Intl.RelativeTimeFormat} Formatter.
+ */
+function getFormatter( locale ) {
+ const key = locale || '';
+ if ( ! formatters[ key ] ) {
+ formatters[ key ] = new Intl.RelativeTimeFormat( locale, { numeric: 'auto' } );
+ }
+ return formatters[ key ];
+}
+
+/**
+ * Format an ISO timestamp as a locale-aware relative string.
+ *
+ * @param {string} isoString Instant in ISO-8601 form.
+ * @param {number} now Comparison time in milliseconds.
+ * @return {string} Relative time, or an empty string when the instant is invalid.
+ */
+export default function formatRelativeTime( isoString, now = Date.now() ) {
+ const date = new Date( isoString );
+
+ if ( Number.isNaN( date.getTime() ) ) {
+ return '';
+ }
+
+ const formatter = getFormatter( resolveLocale() );
+ let duration = ( date.getTime() - now ) / 1000;
+
+ for ( let i = 0; i < DIVISIONS.length; i++ ) {
+ const division = DIVISIONS[ i ];
+ if ( Math.abs( duration ) < division.amount ) {
+ return formatter.format( Math.round( duration ), division.unit );
+ }
+ duration /= division.amount;
+ }
+
+ return '';
+}
diff --git a/tests/e2e/admin-ui-smoke.spec.js b/tests/e2e/admin-ui-smoke.spec.js
index cda0cb74c..8d5a4ef64 100644
--- a/tests/e2e/admin-ui-smoke.spec.js
+++ b/tests/e2e/admin-ui-smoke.spec.js
@@ -11,11 +11,11 @@ import { newAuthedPage } from './helpers/stream-plugin';
/**
* Admin UI smoke test for the Stream plugin.
*
- * Loads the main Stream admin screens and asserts that the records-screen
- * native selects and the jQuery UI datepicker render, and that no uncaught
- * JS errors are emitted. Intended to catch regressions from upstream jQuery
- * or jQuery UI version bumps that the unit / integration suites do not
- * exercise.
+ * Loads the main Stream admin screens and asserts that native selects,
+ * the jQuery UI datepicker, and relative timestamps render, and that no
+ * uncaught JS errors are emitted. Intended to catch regressions from
+ * upstream jQuery or jQuery UI version bumps that the unit / integration
+ * suites do not exercise.
*/
test.describe.configure( { mode: 'serial' } );
@@ -98,6 +98,36 @@ test.describe( 'Admin UI smoke', () => {
await expect( page.locator( '.select2-container' ) ).toHaveCount( 0 );
} );
+ test( 'renders relative timestamps next to the absolute date', async () => {
+ await page.goto( '/wp-admin/admin.php?page=wp_stream' );
+ const time = page
+ .locator( 'table.wp-list-table .column-date time.timeago' )
+ .first();
+ await expect( time ).toBeVisible();
+ await expect( time ).toHaveAttribute( 'datetime', /./ );
+ await expect( time ).toHaveText(
+ /ago|now|yesterday|today|tomorrow|last\s|next\s|in\s|this\s/i,
+ );
+ // Previous presentation: the bold relative string renders alongside the
+ // original absolute date, which stays visible in the cell.
+ await expect( time.locator( 'xpath=ancestor::td[1]' ) ).toHaveText(
+ /\d{4}\/\d{2}\/\d{2}/,
+ );
+ } );
+
+ test( 'uses native selects on Settings exclude rules', async () => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wp_stream_settings&tab=exclude',
+ );
+ await page.locator( '#exclude_rules_new_rule' ).click();
+ const contextSelect = page
+ .locator( '.stream-exclude-list tbody tr:not(.hidden) select.connector_or_context' )
+ .last();
+ await expect( contextSelect ).toBeVisible();
+ await expect( page.locator( '.select2-dropdown' ) ).toHaveCount( 0 );
+ await expect( page.locator( '.select2-container' ) ).toHaveCount( 0 );
+ } );
+
test( 'loads the Settings tab', async () => {
await page.goto( '/wp-admin/admin.php?page=wp_stream_settings' );
await expect( page.locator( 'form' ).first() ).toBeVisible();
@@ -108,6 +138,11 @@ test.describe( 'Admin UI smoke', () => {
const list = page.locator( '.wp-list-table' );
const empty = page.locator( '.no-items, .post-state' );
await expect( list.or( empty ).first() ).toBeVisible();
+ await expect( page.locator( '.select2-dropdown' ) ).toHaveCount( 0 );
+ const bulkWidth = await page
+ .locator( '#bulk-action-selector-top' )
+ .evaluate( ( el ) => parseFloat( window.getComputedStyle( el ).width ) );
+ expect( bulkWidth ).toBeLessThan( 250 );
} );
test( 'new alert type list includes webhook and omits IFTTT', async () => {
diff --git a/tests/e2e/settings-save.spec.js b/tests/e2e/settings-save.spec.js
index 5d754b30a..19139b15c 100644
--- a/tests/e2e/settings-save.spec.js
+++ b/tests/e2e/settings-save.spec.js
@@ -19,7 +19,7 @@ import {
const SETTINGS_URL =
'/wp-admin/network/admin.php?page=wp_stream_network_settings';
-const EXCLUDE_IP = '203.0.113.44';
+const EXCLUDE_IP = '198.51.100.44';
/** Captured before mutation so afterAll can restore the live install. */
let originalTtl = null;
@@ -47,14 +47,12 @@ test.describe( 'Settings save', () => {
await page.getByRole( 'link', { name: 'Exclude' } ).click();
await page.locator( '#exclude_rules_new_rule' ).click();
- await page.evaluate( ( ip ) => {
- const select = document.querySelector(
- '.stream-exclude-list tbody tr:not(.hidden):not(.helper) select.ip_address',
- );
- const option = new Option( ip, ip, true, true );
- select.appendChild( option );
- window.jQuery( select ).trigger( 'change' );
- }, EXCLUDE_IP );
+ const ipInput = page
+ .locator(
+ '.stream-exclude-list tbody tr:not(.hidden):not(.helper) input.ip_address',
+ )
+ .last();
+ await ipInput.fill( EXCLUDE_IP );
await page.getByRole( 'link', { name: 'Advanced' } ).click();
const cron = page.locator(
@@ -84,10 +82,10 @@ test.describe( 'Settings save', () => {
await expect(
page
.locator(
- `.stream-exclude-list select.ip_address option[value="${ EXCLUDE_IP }"]`,
+ '.stream-exclude-list tbody tr:not(.hidden):not(.helper) input.ip_address',
)
- .first(),
- ).toBeAttached();
+ .last(),
+ ).toHaveValue( EXCLUDE_IP );
await page.getByRole( 'link', { name: 'Advanced' } ).click();
if ( cronWasChecked ) {
diff --git a/tests/phpunit/Admin_Assets_Test.php b/tests/phpunit/Admin_Assets_Test.php
index 6c19ed9ec..744827e3c 100644
--- a/tests/phpunit/Admin_Assets_Test.php
+++ b/tests/phpunit/Admin_Assets_Test.php
@@ -66,13 +66,12 @@ public function test_admin_enqueue_scripts() {
$this->assertTrue( wp_style_is( 'wp-stream-admin' ), 'wp-stream-admin style is enqueued' );
- // select2 remains enqueued via the admin-exclude bundle dependency until
- // that screen migrates off it; the records screen itself no longer runs
- // any select2 initialization (see the e2e native-selects assertions).
- $this->assertTrue( wp_script_is( 'wp-stream-select2' ), 'wp-stream-select2 script is enqueued' );
- $this->assertTrue( wp_script_is( 'wp-stream-select2-en' ), 'wp-stream-select2-en script is enqueued' );
- $this->assertTrue( wp_script_is( 'wp-stream-jquery-timeago' ), 'wp-stream-jquery-timeago script is enqueued' );
- $this->assertTrue( wp_script_is( 'wp-stream-jquery-timeago-en' ), 'wp-stream-jquery-timeago-en script is enqueued' );
+ // The select2 and jquery-timeago vendor scripts are no longer enqueued
+ // anywhere: every screen uses native selects and Intl.RelativeTimeFormat.
+ $this->assertFalse( wp_script_is( 'wp-stream-select2' ), 'wp-stream-select2 script is not enqueued' );
+ $this->assertFalse( wp_script_is( 'wp-stream-select2-en' ), 'wp-stream-select2-en script is not enqueued' );
+ $this->assertFalse( wp_script_is( 'wp-stream-jquery-timeago' ), 'wp-stream-jquery-timeago script is not enqueued' );
+ $this->assertFalse( wp_script_is( 'wp-stream-jquery-timeago-en' ), 'wp-stream-jquery-timeago-en script is not enqueued' );
$this->assertTrue( wp_script_is( 'wp-stream-admin' ), 'wp-stream-admin script is enqueued' );
$this->assertTrue( wp_script_is( 'wp-stream-live-updates' ), 'wp-stream-live-updates script is enqueued' );
diff --git a/tests/phpunit/Alerts_List_Test.php b/tests/phpunit/Alerts_List_Test.php
index 2a3ea684a..339988fff 100644
--- a/tests/phpunit/Alerts_List_Test.php
+++ b/tests/phpunit/Alerts_List_Test.php
@@ -65,6 +65,64 @@ public function test_enqueue_scripts() {
/**
* Test save_alert_inline_edit method.
*/
+ public function test_save_alert_inline_edit_regenerates_alert_title() {
+ $alerts_list = new Alerts_List( $this->plugin );
+ $post_id = wp_insert_post(
+ array(
+ 'post_type' => Alerts::POST_TYPE,
+ 'post_title' => 'Admin > Any Context > Any Action',
+ )
+ );
+
+ $user_id = self::factory()->user->create(
+ array(
+ 'role' => 'administrator',
+ 'display_name' => 'Zelly Regenerated',
+ )
+ );
+ wp_set_current_user( $user_id );
+
+ $_POST['post_type'] = Alerts::POST_TYPE;
+ $_POST['wp_stream_trigger_author'] = (string) $user_id;
+ $_POST['wp_stream_trigger_connector_or_context'] = '';
+ $_POST['wp_stream_trigger_action'] = '';
+ $_POST['wp_stream_alert_type'] = 'none';
+ $_POST['wp_stream_alert_status'] = 'wp_stream_enabled';
+ $_POST[ Alerts::POST_TYPE . '_edit_nonce' ] = wp_create_nonce( plugin_basename( $this->plugin->locations['dir'] . 'classes/class-alerts-list.php' ) );
+
+ $data = array();
+ $postarr = array(
+ 'ID' => $post_id,
+ 'post_type' => Alerts::POST_TYPE,
+ );
+
+ $filtered = $alerts_list->save_alert_inline_edit( $data, $postarr );
+
+ $this->assertStringContainsString(
+ 'Zelly Regenerated',
+ $filtered['post_title'],
+ 'Editing a trigger should regenerate the alert title shown in the list'
+ );
+ $this->assertStringNotContainsString(
+ 'Admin',
+ $filtered['post_title'],
+ 'Regenerated title should not keep the creation-time author'
+ );
+
+ $alert_meta = get_post_meta( $post_id, 'alert_meta', true );
+ $this->assertSame( (string) $user_id, $alert_meta['trigger_author'], 'Trigger author not saved' );
+
+ unset(
+ $_POST['post_type'],
+ $_POST['wp_stream_trigger_author'],
+ $_POST['wp_stream_trigger_connector_or_context'],
+ $_POST['wp_stream_trigger_action'],
+ $_POST['wp_stream_alert_type'],
+ $_POST['wp_stream_alert_status'],
+ $_POST[ Alerts::POST_TYPE . '_edit_nonce' ]
+ );
+ }
+
public function test_save_alert_inline_edit() {
$alerts_list = new Alerts_List( $this->plugin );
$post_id = wp_insert_post(
diff --git a/tests/phpunit/Log_Test.php b/tests/phpunit/Log_Test.php
index 0c7732018..db462c7aa 100644
--- a/tests/phpunit/Log_Test.php
+++ b/tests/phpunit/Log_Test.php
@@ -180,5 +180,17 @@ public function test_can_match_record_id_address() {
'Record IP address is one of the IP addresses in the rule'
)
);
+
+ $this->assertTrue(
+ $this->plugin->log->record_matches_rules(
+ array(
+ 'ip_address' => '1.1.1.1',
+ ),
+ array(
+ 'ip_address' => '8.8.8.8, 1.1.1.1',
+ )
+ ),
+ 'Whitespace after commas in a free-text IP list must not break matching'
+ );
}
}
diff --git a/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php b/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php
index 33c4a495d..a9d17de85 100644
--- a/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php
+++ b/tests/phpunit/unit/Alerts_Admin_UI_Unit_Test.php
@@ -187,9 +187,10 @@ public function test_display_submit_box_returns_early_when_post_empty() {
}
public function test_display_notification_box_without_post_renders_type_select() {
- $none = Mockery::mock();
- $none->slug = 'none';
- $none->name = 'Do Nothing';
+ $none = Mockery::mock();
+ $none->slug = 'none';
+ $none->name = 'Do Nothing';
+ $none->creatable = true;
$none->shouldReceive( 'display_fields' )->once()->with( array() );
$this->plugin->alerts->alert_types['none'] = $none;
@@ -283,12 +284,11 @@ public function test_register_scripts_enqueues_alerts_asset_on_list_screen() {
)
);
Functions\when( 'wp_create_nonce' )->justReturn( 'stream-nonce' );
- $this->plugin->shouldReceive( 'with_select2' )->once()->andReturn( array( 'select2' ) );
$this->plugin->shouldReceive( 'enqueue_asset' )
->once()
->with(
'alerts',
- array( array( 'select2' ), 'inline-edit-post' ),
+ array( 'inline-edit-post' ),
Mockery::on(
static function ( $l10n ) {
return isset( $l10n['getActionsNonce'] ) && 'stream-nonce' === $l10n['getActionsNonce'];
diff --git a/tests/phpunit/unit/Form_Generator_Unit_Test.php b/tests/phpunit/unit/Form_Generator_Unit_Test.php
new file mode 100644
index 000000000..c2fe07952
--- /dev/null
+++ b/tests/phpunit/unit/Form_Generator_Unit_Test.php
@@ -0,0 +1,211 @@
+stubTranslationFunctions();
+ $this->stubEscapeFunctions();
+ Functions\when( 'selected' )->alias( array( self::class, 'selected_stub' ) );
+ Functions\when( 'wp_parse_args' )->alias( array( self::class, 'wp_parse_args_stub' ) );
+
+ $this->generator = new Form_Generator();
+ }
+
+ /**
+ * Render a grouped_select field.
+ *
+ * @param string|array $value Current value(s).
+ * @param array $options Option tree.
+ * @param array $extra Extra args (placeholder, multiple).
+ * @return string HTML.
+ */
+ private function render_select( $value, array $options, array $extra = array() ) {
+ $data = array();
+ if ( ! empty( $extra['placeholder'] ) ) {
+ $data['placeholder'] = $extra['placeholder'];
+ }
+
+ return $this->generator->render_field(
+ 'grouped_select',
+ array(
+ 'name' => 'test_select',
+ 'value' => $value,
+ 'options' => $options,
+ 'classes' => 'test-class',
+ 'data' => $data,
+ ),
+ false
+ );
+ }
+
+ public function test_parent_and_children_render_as_flat_options() {
+ $html = $this->render_select(
+ '',
+ array(
+ array(
+ 'value' => 'posts',
+ 'text' => 'Posts',
+ 'children' => array(
+ array(
+ 'value' => 'post',
+ 'text' => 'Single Post',
+ ),
+ ),
+ ),
+ )
+ );
+
+ $this->assertStringContainsString( '