diff --git a/README.txt b/README.txt
index cf1579f..de540dd 100644
--- a/README.txt
+++ b/README.txt
@@ -44,6 +44,7 @@ OWA 2.x is supported in beta: set OWA Server Version to 2.x. The plugin then aut
* Multisite network mode (2.x): Network Admin > OWA takes the OWA endpoint and one application password for every site.
* Each site then chooses its Website ID from the Profiles that share its domain.
* Or the network configures each site's Profile in a mapping table, where each site starts on the Profile of exactly its domain. Opening the network settings page fetches the Profiles from OWA as it reads the network's sites, and marks new sites and new Profiles until the next save.
+ * In that mode each site also has a Tracking switch in the table; the network decides, and the site's own Enable OWA is hidden.
* New setting in both versions: Don't Track Logged-in Users.
* The settings pages are rebuilt with WordPress components (React). They save through the plugin's REST routes; keys and passwords are never sent back to the browser, and an invalid entry is reported under its field without saving anything.
diff --git a/js/lib/fields.js b/js/lib/fields.js
index 948e615..67cc667 100644
--- a/js/lib/fields.js
+++ b/js/lib/fields.js
@@ -16,7 +16,34 @@ export function visibleFields( fields, version, section ) {
return fields.filter( ( field ) => isVisible( field, version ) && ( ! section || field.section === section ) );
}
+/** A field's title and description for the chosen version: 2.x has its own wording for some. */
+export function fieldText( field, version ) {
+ const v2 = version === '2' ? field.v2 || {} : {};
+ return { title: v2.title || field.title, description: v2.description || field.description };
+}
+
+/**
+ * What is left to set up, as the server found it in the saved settings,
+ * worded for the version chosen on the page; none while tracking is off.
+ */
+export function setupNotices( payload, values ) {
+ if ( ! Number( values.enable ) ) {
+ return [];
+ }
+ return ( payload.notices || {} )[ values.owaVersion === '2' ? '2' : '1' ] || [];
+}
+
/** The tabs that have something to show. */
export function visibleSections( sections, fields, version ) {
return sections.filter( ( section ) => visibleFields( fields, version, section.name ).length > 0 );
}
+
+/**
+ * The link to the Website ID's tracking tag screen in OWA shows only for what
+ * is saved: the server builds it from the saved settings, so it is hidden
+ * while the version, the endpoint or the Website ID differs from them.
+ */
+export function showTagSettingsLink( payload, values ) {
+ return Boolean( payload.tagSettingsUrl ) &&
+ [ 'owaVersion', 'owaEndpoint', 'siteId' ].every( ( key ) => values[ key ] === payload.values[ key ] );
+}
diff --git a/js/lib/mapping.js b/js/lib/mapping.js
index a01c8b3..1ca000e 100644
--- a/js/lib/mapping.js
+++ b/js/lib/mapping.js
@@ -2,8 +2,6 @@
* The network settings page's mapping table: each site's Profile.
*/
-export const CHANGED = 'Changed: saved when you save';
-
/** A Profile's option text; one that appeared in OWA since the last save says so. */
export function profileLabel( profile ) {
return profile.new ? `${ profile.label } — new` : profile.label;
@@ -17,13 +15,34 @@ export function profilesFor( profiles, baseDomain ) {
} );
}
-/**
- * The status of a site's row for the value now selected: the server's status
- * while the row is as it was loaded, "changed" once it is not.
- */
-export function rowStatus( blog, value ) {
- const loaded = blog.saved === null ? blog.selected : blog.saved;
- return value === loaded ? blog.status : CHANGED;
+/** A saved row's Profile, as its cell shows it until Edit is clicked. */
+export function savedLabel( profiles, id ) {
+ if ( ! id ) {
+ return 'No Observation Profile';
+ }
+ const profile = profiles.find( ( p ) => p.id === id );
+ return profile ? profile.label : `${ id } (not in the list from OWA)`;
+}
+
+/** A row has a saved Profile: it shows it as text with Edit, and a Tracking switch. */
+export function hasSavedProfile( blog ) {
+ return Boolean( blog.saved );
+}
+
+/** A row shows its dropdown until a Profile is saved for it, or after Edit. */
+export function isEditing( blog, editing ) {
+ return ! hasSavedProfile( blog ) || Boolean( editing[ blog.id ] );
+}
+
+/** The switches to send: only the rows that show one; the server keeps the rest. */
+export function switchesPayload( blogs, switches ) {
+ const out = {};
+ for ( const blog of blogs ) {
+ if ( hasSavedProfile( blog ) && blog.id in switches ) {
+ out[ blog.id ] = switches[ blog.id ];
+ }
+ }
+ return out;
}
/** The selects' values, as the network route takes them. */
@@ -35,6 +54,17 @@ export function mappingPayload( values ) {
return out;
}
+/** The switches as they start: each site's saved on/off. */
+export function initialSwitches( blogs ) {
+ const out = {};
+ for ( const blog of blogs ) {
+ if ( 'enabled' in blog ) {
+ out[ blog.id ] = blog.enabled;
+ }
+ }
+ return out;
+}
+
/** The values the selects start on. */
export function initialMapping( blogs ) {
const out = {};
diff --git a/js/network/index.js b/js/network/index.js
index 47a3d97..e5865b5 100644
--- a/js/network/index.js
+++ b/js/network/index.js
@@ -5,19 +5,19 @@
* network's sites, so every load shows both as they are now; POST saves.
*/
import { createRoot, useEffect, useState } from '@wordpress/element';
-import { Button, Notice, SelectControl, Spinner, TextControl, ToggleControl } from '@wordpress/components';
+import { Button, FormToggle, Notice, SelectControl, Spinner, TextControl, ToggleControl } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { secretHint, secretsPayload } from '../lib/secrets';
-import { CHANGED, initialMapping, mappingPayload, profileLabel, profilesFor, rowStatus } from '../lib/mapping';
+import { hasSavedProfile, initialMapping, initialSwitches, isEditing, mappingPayload, profileLabel, profilesFor, savedLabel, switchesPayload } from '../lib/mapping';
import '../settings/style.css';
const NETWORK = '/owa-wp/v1/network';
-function SitesTable( { payload, mapping, setMapping } ) {
+function SitesTable( { payload, mapping, setMapping, switches, setSwitches, editing, setEditing } ) {
const fetched = payload.sitesFetchedAt
- ? `Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).`
- : 'No Profiles have been fetched from OWA yet.';
+ ? `Observation Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).`
+ : 'No Observation Profiles have been fetched from OWA yet.';
return (
<>
@@ -27,15 +27,15 @@ function SitesTable( { payload, mapping, setMapping } ) {
Site
- Website ID
- { payload.assignProfiles && Status }
+ Observation Profile
+ { payload.assignProfiles && Tracking }
{ payload.blogs.map( ( blog ) => {
if ( ! payload.assignProfiles ) {
return (
-
+
{ blog.host }
{ blog.own || 'not chosen yet' }
@@ -43,9 +43,17 @@ function SitesTable( { payload, mapping, setMapping } ) {
}
const value = mapping[ blog.id ] ?? '';
- const status = rowStatus( blog, value );
+ // the switch (and greying) only once the row has a saved Profile
+ const saved = hasSavedProfile( blog );
+ const enabled = switches[ blog.id ] ?? true;
+ const off = saved && ! enabled;
+ const edit = isEditing( blog, editing );
+
+ const setValue = ( v ) => setMapping( ( current ) => ( { ...current, [ blog.id ]: v } ) );
+ const setEdit = ( on ) => setEditing( ( current ) => ( { ...current, [ blog.id ]: on } ) );
+
const options = [
- { label: '— not tracked —', value: '' },
+ { label: 'Select Profile…', value: '' },
...profilesFor( payload.profiles, blog.baseDomain ).map( ( profile ) => ( { label: profileLabel( profile ), value: profile.id } ) ),
];
@@ -54,20 +62,54 @@ function SitesTable( { payload, mapping, setMapping } ) {
}
return (
-
- { blog.host }
+
+
+ { blog.host }
+ { blog.saved === null && New }
+
- setMapping( ( current ) => ( { ...current, [ blog.id ]: v } ) ) }
- />
+ { edit ? (
+
+
+ { saved && (
+ {
+ setValue( blog.saved );
+ setEdit( false );
+ } }
+ >
+ Cancel
+
+ ) }
+
+ ) : (
+
+ { savedLabel( payload.profiles, blog.saved ) }
+ setEdit( true ) } aria-label={ `Edit the Observation Profile for ${ blog.host }` }>
+ Edit
+
+
+ ) }
+
+
+ { saved && (
+ setSwitches( ( current ) => ( { ...current, [ blog.id ]: ! enabled } ) ) }
+ />
+ ) }
- { status }
);
} ) }
@@ -83,6 +125,8 @@ function NetworkPage() {
const [ assign, setAssign ] = useState( false );
const [ password, setPassword ] = useState( {} );
const [ mapping, setMapping ] = useState( {} );
+ const [ switches, setSwitches ] = useState( {} );
+ const [ editing, setEditing ] = useState( {} );
const [ errors, setErrors ] = useState( {} );
const [ notices, setNotices ] = useState( [] );
const [ busy, setBusy ] = useState( false );
@@ -94,6 +138,8 @@ function NetworkPage() {
setAssign( result.assignProfiles );
setPassword( {} );
setMapping( initialMapping( result.blogs ) );
+ setSwitches( initialSwitches( result.blogs ) );
+ setEditing( {} );
setErrors( problems );
setNotices( [
...( result.saved ? [ { status: 'success', text: 'Settings saved.' } ] : [] ),
@@ -118,6 +164,7 @@ function NetworkPage() {
if ( payload.assignProfiles && assign ) {
data.assignedSiteIds = mappingPayload( mapping );
+ data.enabledSites = switchesPayload( payload.blogs, switches );
}
setBusy( true );
@@ -165,7 +212,7 @@ function NetworkPage() {
__next40pxDefaultSize
__nextHasNoMarginBottom
label="Application Password"
- help="An application password from OWA (My Preferences > Application Passwords). Site admins can list the Profiles its OWA user can see, so use a read-only password of a user limited to this network's Properties."
+ help="An application password from OWA (My Preferences > Application Passwords). Site admins can list the Observation Profiles its OWA user can see, so use a read-only password of a user limited to this network's Properties."
type="password"
autoComplete="off"
value={ password.value || '' }
@@ -185,17 +232,27 @@ function NetworkPage() {
- { payload.active && }
+ { payload.active && (
+
+ ) }
Save Changes
diff --git a/js/settings/index.js b/js/settings/index.js
index c2c9d7f..d4821cd 100644
--- a/js/settings/index.js
+++ b/js/settings/index.js
@@ -19,7 +19,7 @@ import {
} from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
-import { visibleFields, visibleSections } from '../lib/fields';
+import { fieldText, setupNotices, showTagSettingsLink, visibleFields, visibleSections } from '../lib/fields';
import { secretHint, secretsPayload } from '../lib/secrets';
import './style.css';
@@ -30,26 +30,28 @@ function FieldError( { message } ) {
return message ? { message }
: null;
}
-function WebsiteId( { field, value, onChange, sites, payload } ) {
- const options = [ { label: sites.loading ? 'Loading…' : 'Select…', value: '' }, ...sites.choices ];
+function WebsiteId( { field, value, onChange, sites, payload, values } ) {
+ const v2 = values.owaVersion === '2';
+ const text = fieldText( field, values.owaVersion );
+ const options = [ { label: sites.loading ? 'Loading…' : v2 ? 'Select Profile…' : 'Select…', value: '' }, ...sites.choices ];
return (
<>
{ sites.notice && { sites.notice }
}
{ payload.network.note && { payload.network.note }
}
- { payload.tagSettingsUrl && (
+ { showTagSettingsLink( payload, values ) && (
- Configure this Website ID's tracking tag in OWA
+ Configure this Observation Profile's tracking tag in OWA
) }
>
@@ -58,7 +60,8 @@ function WebsiteId( { field, value, onChange, sites, payload } ) {
function Field( { field, payload, values, setValue, secrets, setSecret, sites, error } ) {
const value = values[ field.key ];
- const common = { label: field.title, help: field.description };
+ const text = fieldText( field, values.owaVersion );
+ const common = { label: text.title, help: text.description };
switch ( field.type ) {
case 'radio':
@@ -96,7 +99,7 @@ function Field( { field, payload, values, setValue, secrets, setSecret, sites, e
}
case 'select':
- return setValue( field.key, v ) } sites={ sites } payload={ payload } />;
+ return setValue( field.key, v ) } sites={ sites } payload={ payload } values={ values } />;
default:
return (
@@ -179,6 +182,11 @@ function SettingsPage() {
{ notice.text }
) }
+ { setupNotices( payload, values ).map( ( text ) => (
+
+ { text }
+
+ ) ) }
Settings for integrating WordPress with an existing Open Web Analytics (OWA) server instance.
tab.name ).join() }>
{ ( tab ) => (
diff --git a/js/settings/style.css b/js/settings/style.css
index 3fae519..3101107 100644
--- a/js/settings/style.css
+++ b/js/settings/style.css
@@ -33,10 +33,39 @@
vertical-align: middle;
}
-.owa-wp-network .owa-wp-status.is-changed {
- font-weight: 600;
+/* a site the network has switched off: greyed, its switch still usable */
+.owa-wp-network .owa-wp-sites tr.is-disabled td:not(.owa-wp-tracking) {
+ opacity: 0.45;
+}
+
+.owa-wp-network .owa-wp-profile {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.owa-wp-network .owa-wp-profile .components-base-control {
+ flex: 1;
+}
+
+.owa-wp-network .owa-wp-badge {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 0 6px;
+ border-radius: 2px;
+ background: #dcdcde;
+ color: #1e1e1e;
+ font-size: 11px;
+ line-height: 18px;
+ vertical-align: middle;
}
.owa-wp-actions .components-button + .components-button {
margin-left: 8px;
}
+
+.owa-wp-network .owa-wp-sites .owa-wp-tracking {
+ text-align: right;
+ width: 1%;
+ white-space: nowrap;
+}
diff --git a/owa-plugin.php b/owa-plugin.php
index d7fc4a7..8f6f562 100644
--- a/owa-plugin.php
+++ b/owa-plugin.php
@@ -51,6 +51,9 @@
use owaWp\module;
use owaWp\util;
+// page caches purge a site when its tag changes; Proxy Cache Purge reads its events on init
+owaWp\cache::register();
+
/**
* OWA WordPress Plugin Class
*
@@ -141,6 +144,10 @@ function _init() {
// setup plugin options
$this->initOptions();
+ // the SDK serves the Website ID list on the settings page, which a
+ // site with tracking off needs too
+ $this->initOwaSdk();
+
// register WordPress hooks and filters
if ( $this->getOption('enable') ) {
@@ -156,16 +163,6 @@ function _init() {
}
- // init the sdk (used for the Website ID list)
- $this->initOwaSdk();
-
- // remove this if uneeded
- if ( ! $this->isOwaReadyToTrack() ) {
-
- $this->adminMsgs[] = ['message' => 'Open Web Analytics requires that you select a Site ID before tracking can begin.', 'class' => 'notice-warning'];
-
- }
-
add_action('admin_notices', array( $this, 'showNag') );
}
@@ -200,9 +197,20 @@ function showNag() {
);
- if ( $this->adminMsgs ) {
+ $messages = $this->adminMsgs;
+
+ // the settings page shows these itself, for the version chosen on it
+ if ( ! $this->isSettingsScreen() ) {
+
+ foreach ( $this->setupNotices() as $notice ) {
+
+ $messages[] = [ 'message' => $notice, 'class' => 'notice-warning' ];
+ }
+ }
+
+ if ( $messages ) {
- foreach ( $this->adminMsgs as $msg ) {
+ foreach ( $messages as $msg ) {
$message = wp_kses( $msg['message'], $allowed_html );
@@ -418,6 +426,9 @@ function applyNetworkOptions() {
if ( $network['assignProfiles'] ) {
$this->options['siteId'] = $network['assignedSiteIds'][ get_current_blog_id() ] ?? '';
+
+ // the network's switch for this site is the only on/off
+ $this->options['enable'] = owaWp\network::isSiteOn( $network['disabledSiteIds'], get_current_blog_id() ) ? 1 : 0;
}
}
@@ -456,7 +467,12 @@ function getOwaVersion() {
return (string) $this->getOption( 'owaVersion' ) === '2' ? 2 : 1;
}
- function isOwaReadyToTrack() {
+ /**
+ * @param int|null $version 1 or 2; the saved version by default
+ */
+ function isOwaReadyToTrack( $version = null ) {
+
+ $version = $version ?? $this->getOwaVersion();
if ( ! $this->getOption( 'owaEndpoint' ) || ! $this->getOption( 'siteId' ) ) {
@@ -464,7 +480,7 @@ function isOwaReadyToTrack() {
}
// 2.x loads .js, so the ID has to be a file name OWA can publish
- if ( $this->getOwaVersion() === 2 && ! preg_match( self::BUNDLE_SITE_ID_PATTERN, (string) $this->getOption( 'siteId' ) ) ) {
+ if ( $version === 2 && ! preg_match( self::BUNDLE_SITE_ID_PATTERN, (string) $this->getOption( 'siteId' ) ) ) {
return false;
}
@@ -474,15 +490,19 @@ function isOwaReadyToTrack() {
/**
* Credentials for the Website ID list are present.
+ *
+ * @param int|null $version 1 or 2; the saved version by default
*/
- function isOwaSdkReady() {
+ function isOwaSdkReady( $version = null ) {
+
+ $version = $version ?? $this->getOwaVersion();
if ( ! $this->getOption( 'owaEndpoint' ) ) {
return false;
}
- if ( $this->getOwaVersion() === 2 ) {
+ if ( $version === 2 ) {
return (bool) $this->getOption( 'appPassword' );
}
@@ -500,10 +520,6 @@ function initOwaSdk() {
if ( ! $this->isOwaSdkReady() ) {
- $this->addNag( $this->getOwaVersion() === 2
- ? 'Enter your OWA Endpoint and an Application Password to load the list of websites from OWA.'
- : 'You must enter your OWA API key, Secret Auth key, and Endpoint settings.', 'notice-warning' );
-
return;
}
@@ -681,7 +697,7 @@ function filterSitesForNetwork( array $sites ) {
if ( $assigned === '' ) {
- $this->sitesNotice = 'The network administrator has not assigned this site an OWA Profile, so it is not tracked.';
+ $this->sitesNotice = 'The network administrator has not assigned this site an Observation Profile, so it is not tracked.';
}
return array_filter( $sites, fn( $site ) => owaWp\network::rowSiteId( $site ) === $assigned );
@@ -699,8 +715,50 @@ function getNetworkNote() {
}
return owaWp\network::options()['assignProfiles']
- ? sprintf( 'The network administrator manages the OWA server (%s) and configures this site\'s Profile.', $this->getOption( 'owaEndpoint' ) )
- : sprintf( 'The network administrator manages the OWA server (%s) and its credentials. The list shows the Profiles that share this site\'s domain.', $this->getOption( 'owaEndpoint' ) );
+ ? sprintf( 'The network administrator manages the OWA server (%s), configures this site\'s Observation Profile and has tracking on this site turned %s.', $this->getOption( 'owaEndpoint' ), $this->getOption( 'enable' ) ? 'on' : 'off' )
+ : sprintf( 'The network administrator manages the OWA server (%s) and its credentials. The list shows the Observation Profiles that share this site\'s domain.', $this->getOption( 'owaEndpoint' ) );
+ }
+
+ /**
+ * What is left to set up before this site is tracked, from the saved
+ * settings, worded for a version (the saved one by default).
+ *
+ * @param int|null $version 1 or 2
+ * @return string[]
+ */
+ function setupNotices( $version = null ) {
+
+ $version = $version ?? $this->getOwaVersion();
+ $notices = [];
+
+ if ( ! $this->isOwaSdkReady( $version ) ) {
+
+ $notices[] = $version === 2
+ ? 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.'
+ : 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.';
+ }
+
+ // where the network sets the Observation Profile, the site admin cannot, and the page says so
+ $assigned = owaWp\network::isActive() && owaWp\network::options()['assignProfiles'];
+
+ if ( ! $assigned && ! $this->isOwaReadyToTrack( $version ) ) {
+
+ $notices[] = $version === 2
+ ? 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.'
+ : 'Open Web Analytics requires that you select a Website ID before tracking can begin.';
+ }
+
+ return $notices;
+ }
+
+ /**
+ * The site's OWA > Settings screen.
+ */
+ function isSettingsScreen() {
+
+ $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
+
+ return $screen && $screen->id === 'toplevel_page_owa-wordpress';
}
/**
@@ -769,7 +827,7 @@ public function registerOptions() {
'title' => 'OWA Server Version',
'page_name' => 'owa-wordpress',
'section' => 'general',
- 'description' => 'Which Open Web Analytics server this site reports to. 2.x (beta) loads the Website ID's tracking bundle; its tracker settings are made on the OWA server.',
+ 'description' => 'Which Open Web Analytics server this site reports to. 2.x (beta) loads the Observation Profile's tracking bundle; its tracker settings are made on the OWA server.',
'label_for' => 'OWA server version',
'options' => [ '1' => '1.x', '2' => '2.x (beta)' ],
'error_message' => ''
@@ -864,9 +922,11 @@ public function registerOptions() {
'field' => array(
'type' => 'select',
'title' => 'Website ID',
+ 'title_v2' => 'Observation Profile',
'page_name' => 'owa-wordpress',
'section' => 'general',
'description' => 'Select the ID of the website you want to track. New tracked websites can be added via the OWA server admin interface.',
+ 'description_v2' => 'Select the Observation Profile you want to track. New Observation Profiles can be added in OWA.',
'label_for' => 'Tracked website ID',
'length' => 90,
'error_message' => '',
@@ -976,6 +1036,7 @@ public function registerOptions() {
if ( owaWp\network::options()['assignProfiles'] ) {
$managed[] = 'siteId';
+ $managed[] = 'enable';
$settings['siteId']['field']['readonly'] = true;
}
@@ -1045,7 +1106,7 @@ function getSitesFromOwa( $options = [] ) {
if ( ! is_array( $sites ) ) {
$errors = $sites_client->getLastErrors();
- $this->sitesNotice = 'Could not load the list of websites from OWA: ' . ( $errors ? implode( '; ', $errors ) : 'no response' );
+ $this->sitesNotice = sprintf( 'Could not load the list of %s from OWA: ', $this->getOwaVersion() === 2 ? 'Observation Profiles' : 'websites' ) . ( $errors ? implode( '; ', $errors ) : 'no response' );
}
}
diff --git a/src/cache.php b/src/cache.php
new file mode 100644
index 0000000..609925a
--- /dev/null
+++ b/src/cache.php
@@ -0,0 +1,92 @@
+ $events
+ * @return array
+ */
+ public static function addPurgeEvent( $events ) {
+
+ $events = (array) $events;
+ $events[] = self::TAG_CHANGED;
+
+ return array_values( array_unique( $events ) );
+ }
+
+ /**
+ * Fires TAG_CHANGED for a site, in that site's context so a purge is of
+ * its own pages.
+ */
+ public static function tagChanged( $blog_id = 0 ) {
+
+ if ( ! is_multisite() ) {
+
+ do_action( self::TAG_CHANGED, 1 );
+
+ return;
+ }
+
+ $blog_id = (int) ( $blog_id ?: get_current_blog_id() );
+ $switch = $blog_id !== get_current_blog_id();
+
+ if ( $switch ) {
+
+ switch_to_blog( $blog_id );
+ }
+
+ do_action( self::TAG_CHANGED, $blog_id );
+
+ if ( $switch ) {
+
+ restore_current_blog();
+ }
+ }
+
+ /**
+ * The part of a set of settings that decides the tag, to compare before
+ * and after a save.
+ *
+ * @param array $options
+ */
+ public static function tagState( array $options ) {
+
+ $state = [];
+
+ foreach ( self::TAG_SETTINGS as $key ) {
+
+ $state[ $key ] = (string) ( $options[ $key ] ?? '' );
+ }
+
+ return $state;
+ }
+}
+
+?>
diff --git a/src/network.php b/src/network.php
index 77395f1..d0a873c 100644
--- a/src/network.php
+++ b/src/network.php
@@ -26,18 +26,29 @@ class network {
const COUNTRY_SECOND_LEVELS = [ 'ac', 'co', 'com', 'edu', 'gob', 'go', 'gov', 'ltd', 'me', 'mil', 'ne', 'net', 'nic', 'or', 'org', 'plc' ];
/**
- * @return array{owaEndpoint: string, appPassword: string, assignProfiles: bool, assignedSiteIds: array, sites: array, sitesFetchedAt: int, newProfileIds: string[]}
+ * @return array{owaEndpoint: string, appPassword: string, assignProfiles: bool, assignedSiteIds: array, disabledSiteIds: int[], sites: array, sitesFetchedAt: int, newProfileIds: string[]}
*/
public static function options() {
$stored = function_exists( 'get_site_option' ) ? get_site_option( self::OPTION, [] ) : [];
- $stored = is_array( $stored ) ? $stored : [];
+
+ return self::normalize( is_array( $stored ) ? $stored : [] );
+ }
+
+ /**
+ * The network option with every key, typed.
+ *
+ * @param array $stored
+ */
+ public static function normalize( array $stored ) {
return [
'owaEndpoint' => (string) ( $stored['owaEndpoint'] ?? '' ),
'appPassword' => (string) ( $stored['appPassword'] ?? '' ),
'assignProfiles' => ! empty( $stored['assignProfiles'] ),
'assignedSiteIds' => array_map( 'strval', (array) ( $stored['assignedSiteIds'] ?? [] ) ),
+ // sites the network has switched off (network-configured mode); any other is on
+ 'disabledSiteIds' => array_map( 'intval', (array) ( $stored['disabledSiteIds'] ?? [] ) ),
'sites' => (array) ( $stored['sites'] ?? [] ),
'sitesFetchedAt' => (int) ( $stored['sitesFetchedAt'] ?? 0 ),
// Profiles that appeared in OWA since the last save
@@ -51,16 +62,66 @@ public static function options() {
*/
public static function isActive() {
+ // a single site never reads the network option
if ( ! function_exists( 'is_multisite' ) || ! is_multisite() ) {
return false;
}
- $options = self::options();
+ return self::isActiveFor( self::options() );
+ }
+
+ /**
+ * Whether these network settings put the network in network mode.
+ *
+ * @param array $options normalized
+ */
+ public static function isActiveFor( array $options ) {
+
+ if ( ! function_exists( 'is_multisite' ) || ! is_multisite() ) {
+
+ return false;
+ }
return $options['owaEndpoint'] !== '' && $options['appPassword'] !== '';
}
+ /**
+ * What decides each site's tag under these network settings: the
+ * network's server, Profile and switch in network mode, the site's own
+ * settings otherwise (as owaWp_plugin::applyNetworkOptions() applies them).
+ *
+ * @param array $options normalized
+ * @param array $blog_hosts
+ * @return array>
+ */
+ public static function tagStates( array $options, array $blog_hosts ) {
+
+ $states = [];
+
+ foreach ( array_keys( $blog_hosts ) as $blog_id ) {
+
+ $own = get_blog_option( $blog_id, 'owa_wp', [] );
+ $state = is_array( $own ) ? $own : [];
+
+ if ( self::isActiveFor( $options ) ) {
+
+ $state['owaVersion'] = '2';
+ $state['owaEndpoint'] = $options['owaEndpoint'];
+
+ if ( $options['assignProfiles'] ) {
+
+ $state['siteId'] = $options['assignedSiteIds'][ $blog_id ] ?? '';
+ $state['enable'] = self::isSiteOn( $options['disabledSiteIds'], $blog_id ) ? 1 : 0;
+ }
+ }
+
+ $states[ $blog_id ] = cache::tagState( $state );
+ }
+
+ return $states;
+ }
+
/**
* The host of a URL or bare domain, lower case: 'https://Example.com/x' and
* 'example.com' are both 'example.com'.
@@ -215,7 +276,7 @@ public static function cacheRows( array $sites ) {
/**
* The Profile a site's row starts on: the network's stored choice for it
- * ('' is "not tracked"), else the one Profile whose domain is exactly the
+ * ('' is "no Profile"), else the one Profile whose domain is exactly the
* site's.
*
* @param array $sites
@@ -236,7 +297,7 @@ public static function defaultSiteIdFor( array $sites, $host, array $map, $blog_
/**
* The submitted Profile for each site, checked against the fetched list.
*
- * @param array $submitted blog ID => site ID ('' = not tracked)
+ * @param array $submitted blog ID => site ID ('' = no Profile)
* @param array $sites cached rows
* @param array $map stored mapping
* @param array $blog_hosts
@@ -262,7 +323,7 @@ public static function sanitizeMapping( array $submitted, array $sites, array $m
} else {
- $errors[] = sprintf( '%s: %s is not in the list fetched from OWA, so its Profile was not changed.', $blog_hosts[ $blog_id ], $site_id );
+ $errors[] = sprintf( '%s: %s is not in the list fetched from OWA, so its Observation Profile was not changed.', $blog_hosts[ $blog_id ], $site_id );
}
}
@@ -285,29 +346,49 @@ public static function compareProfiles( array $before, array $after ) {
}
/**
- * A site row's state: saved, new to the network (not saved yet), or saved
- * on a Profile OWA no longer lists.
+ * Whether the network has a site switched on. A site not switched off,
+ * including one new to the network, is on.
*
- * @param array $sites
- * @param array $map
+ * @param int[] $disabled
*/
- public static function rowStatus( array $sites, array $map, $blog_id, $selected ) {
+ public static function isSiteOn( array $disabled, $blog_id ) {
- if ( ! array_key_exists( (int) $blog_id, $map ) ) {
+ return ! in_array( (int) $blog_id, $disabled, true );
+ }
- return $selected !== ''
- ? 'New site: starts on its exact match, saved when you save'
- : 'New site: no exact match, not tracked until you choose a Profile';
- }
+ /**
+ * The sites switched off after a save: each submitted switch for a site of
+ * the network, the stored state for the rest.
+ *
+ * @param array $submitted blog ID => on (bool)
+ * @param int[] $disabled stored
+ * @param array $blog_hosts
+ * @return int[]
+ */
+ public static function sanitizeSwitches( array $submitted, array $disabled, array $blog_hosts ) {
- $saved = (string) $map[ (int) $blog_id ];
+ $disabled = array_map( 'intval', $disabled );
- if ( $saved !== '' && $sites && ! isset( $sites[ $saved ] ) ) {
+ foreach ( $submitted as $blog_id => $on ) {
+
+ $blog_id = (int) $blog_id;
+
+ if ( ! isset( $blog_hosts[ $blog_id ] ) ) {
- return 'The saved Profile is no longer in the list from OWA';
+ continue;
+ }
+
+ $disabled = array_values( array_diff( $disabled, [ $blog_id ] ) );
+
+ if ( ! filter_var( $on, FILTER_VALIDATE_BOOLEAN ) ) {
+
+ $disabled[] = $blog_id;
+ }
}
- return 'Saved';
+ sort( $disabled );
+
+ return $disabled;
}
/**
@@ -399,8 +480,8 @@ public static function fetchProfiles( $plugin, array &$options ) {
return [
'info' => [],
- 'error' => 'Could not load the list of websites from OWA: ' . $plugin->getLastSitesError()
- . ( $fetched ? sprintf( '. Showing the Profiles fetched %s UTC.', gmdate( 'Y-m-d H:i', $fetched ) ) : '.' ),
+ 'error' => 'Could not load the list of Observation Profiles from OWA: ' . $plugin->getLastSitesError()
+ . ( $fetched ? sprintf( '. Showing the Observation Profiles fetched %s UTC.', gmdate( 'Y-m-d H:i', $fetched ) ) : '.' ),
];
}
@@ -416,12 +497,12 @@ public static function fetchProfiles( $plugin, array &$options ) {
if ( $before && $change['new'] ) {
- $info[] = 'New in OWA since the last visit: ' . implode( ', ', $change['new'] ) . '.';
+ $info[] = 'New Observation Profiles in OWA since the last visit: ' . implode( ', ', $change['new'] ) . '.';
}
if ( $change['gone'] ) {
- $info[] = 'No longer in OWA: ' . implode( ', ', $change['gone'] ) . '.';
+ $info[] = 'Observation Profiles no longer in OWA: ' . implode( ', ', $change['gone'] ) . '.';
}
return [ 'info' => $info, 'error' => '' ];
@@ -487,6 +568,8 @@ public static function save( array $input, $plugin ) {
}
$info = [];
+ $blog_hosts = is_multisite() ? self::blogHosts() : [];
+ $before = self::tagStates( self::normalize( $stored ), $blog_hosts );
// saving marks everything seen
$options['newProfileIds'] = [];
@@ -506,7 +589,6 @@ public static function save( array $input, $plugin ) {
if ( self::isActive() && ! empty( $options['assignProfiles'] ) ) {
- $blog_hosts = self::blogHosts();
$sites = (array) ( $options['sites'] ?? [] );
$map = array_map( 'strval', (array) ( $options['assignedSiteIds'] ?? [] ) );
@@ -521,10 +603,24 @@ public static function save( array $input, $plugin ) {
}
$options['assignedSiteIds'] = self::fillDefaults( $sites, $map, $blog_hosts );
+
+ if ( isset( $input['enabledSites'] ) && is_array( $input['enabledSites'] ) ) {
+
+ $options['disabledSiteIds'] = self::sanitizeSwitches( $input['enabledSites'], (array) ( $options['disabledSiteIds'] ?? [] ), $blog_hosts );
+ }
}
update_site_option( self::OPTION, $options );
+ // purge the pages of each site whose tag this changed
+ foreach ( self::tagStates( self::normalize( $options ), $blog_hosts ) as $blog_id => $state ) {
+
+ if ( $state !== ( $before[ $blog_id ] ?? null ) ) {
+
+ cache::tagChanged( $blog_id );
+ }
+ }
+
return [ $options, $errors, $info ];
}
}
diff --git a/src/rest.php b/src/rest.php
index 1db253d..440f56d 100644
--- a/src/rest.php
+++ b/src/rest.php
@@ -68,6 +68,11 @@ public static function settingsPayload( $plugin ) {
'type' => $type,
'title' => html_entity_decode( (string) $field['title'], ENT_QUOTES, 'UTF-8' ),
'description' => html_entity_decode( (string) ( $field['description'] ?? '' ), ENT_QUOTES, 'UTF-8' ),
+ // wording for 2.x, where it differs (the Website ID is an Observation Profile there)
+ 'v2' => array_map(
+ fn( $text ) => html_entity_decode( (string) $text, ENT_QUOTES, 'UTF-8' ),
+ array_filter( [ 'title' => $field['title_v2'] ?? '', 'description' => $field['description_v2'] ?? '' ] )
+ ),
'section' => $field['section'] ?? 'general',
'version' => $field['owa_version'] ?? null,
'options' => self::choices( $field['options'] ?? [] ),
@@ -98,6 +103,8 @@ public static function settingsPayload( $plugin ) {
'secrets' => $secrets,
'sections' => $sections,
'network' => [ 'active' => network::isActive(), 'note' => $plugin->getNetworkNote() ],
+ // what is left to set up, worded for each version, from the saved settings
+ 'notices' => [ '1' => $plugin->setupNotices( 1 ), '2' => $plugin->setupNotices( 2 ) ],
'tagSettingsUrl' => $plugin->getOwaVersion() === 2 && $plugin->getOption( 'owaEndpoint' ) && $plugin->getOption( 'siteId' ) ? $plugin->getTagSettingsUrl() : '',
];
}
@@ -137,10 +144,17 @@ public static function saveSettings( $plugin, array $input ) {
return new \WP_Error( 'owa_wp_invalid', 'The settings were not saved.', [ 'status' => 400, 'errors' => $errors ] );
}
+ $before = cache::tagState( (array) $plugin->options );
+
update_option( 'owa_wp', $out );
$plugin->reloadOptions();
+ if ( cache::tagState( (array) $plugin->options ) !== $before ) {
+
+ cache::tagChanged();
+ }
+
return self::settingsPayload( $plugin ) + [ 'saved' => true ];
}
@@ -237,7 +251,7 @@ public static function sanitizeSettings( array $schema, array $input, array $sto
&& (string) ( $out['siteId'] ?? '' ) !== ''
&& ! preg_match( \owaWp_plugin::BUNDLE_SITE_ID_PATTERN, (string) $out['siteId'] ) ) {
- $errors['siteId'] = 'OWA 2.x cannot publish a tracking bundle for this Website ID.';
+ $errors['siteId'] = 'OWA 2.x cannot publish a tracking bundle for this Observation Profile.';
}
return [ $out, $errors ];
@@ -354,7 +368,7 @@ public static function networkPayload() {
$blog += [
'saved' => array_key_exists( $blog_id, $map ) ? (string) $map[ $blog_id ] : null,
'selected' => $selected,
- 'status' => network::rowStatus( $options['sites'], $map, $blog_id, $selected ),
+ 'enabled' => network::isSiteOn( $options['disabledSiteIds'], $blog_id ),
];
} else {
diff --git a/tests/Integration/OwaV2ServerTest.php b/tests/Integration/OwaV2ServerTest.php
index a114a95..d03ce1a 100644
--- a/tests/Integration/OwaV2ServerTest.php
+++ b/tests/Integration/OwaV2ServerTest.php
@@ -98,7 +98,7 @@ public function testAWrongApplicationPasswordIsReportedUnderThePicker(): void {
$sites = $plugin->getSitesFromOwa();
$this->assertSame( [ 'saved1' ], array_keys( $sites ) );
- $this->assertStringStartsWith( 'Could not load the list of websites from OWA: ', $plugin->getSitesNotice() );
+ $this->assertStringStartsWith( 'Could not load the list of Observation Profiles from OWA: ', $plugin->getSitesNotice() );
}
public function testTheTrackingTagLinkIsARealScreen(): void {
diff --git a/tests/NetworkTest.php b/tests/NetworkTest.php
index ecd51e2..5632c9e 100644
--- a/tests/NetworkTest.php
+++ b/tests/NetworkTest.php
@@ -25,8 +25,25 @@ protected function setUp(): void {
Functions\when( 'get_site_option' )->alias( fn( $name, $default = false ) => $name === network::OPTION ? $this->network : $default );
Functions\when( 'update_site_option' )->alias( function ( $name, $value ) { $this->network = $value; return true; } );
+
+ // a site's own settings, and the purges fired (blog, and the site switched to when it fired)
+ $this->ownOptions = [];
+ $this->purged = [];
+ $this->currentBlog = 1;
+ Functions\when( 'get_blog_option' )->alias( fn( $blog_id ) => $this->ownOptions[ $blog_id ] ?? [] );
+ Functions\when( 'get_current_blog_id' )->alias( fn() => $this->currentBlog );
+ Functions\when( 'switch_to_blog' )->alias( function ( $blog_id ) { $this->switchedFrom = $this->currentBlog; $this->currentBlog = $blog_id; } );
+ Functions\when( 'restore_current_blog' )->alias( function () { $this->currentBlog = $this->switchedFrom; } );
+ \Brain\Monkey\Actions\expectDone( \owaWp\cache::TAG_CHANGED )->zeroOrMoreTimes()->whenHappen( function ( $blog_id ) {
+ $this->purged[] = [ $blog_id, $this->currentBlog ];
+ } );
}
+ private array $ownOptions = [];
+ private array $purged = [];
+ private int $currentBlog = 1;
+ private int $switchedFrom = 1;
+
private function networkMode( array $options = [] ): void {
Functions\when( 'is_multisite' )->justReturn( true );
@@ -36,7 +53,7 @@ private function networkMode( array $options = [] ): void {
private function onBlog( int $blog_id, string $domain ): void {
- Functions\when( 'get_current_blog_id' )->justReturn( $blog_id );
+ $this->currentBlog = $blog_id;
Functions\when( 'get_site' )->justReturn( (object) [ 'blog_id' => $blog_id, 'domain' => $domain ] );
}
@@ -133,7 +150,7 @@ public function testASubmittedProfileMustBeInTheFetchedList(): void {
);
$this->assertSame( [ 3 => 'OWA-old', 1 => 'OWA-0' ], $map, 'Blog 9 is not in the network.' );
- $this->assertSame( [ 'archive.peteradamsphoto.com: OWA-gone is not in the list fetched from OWA, so its Profile was not changed.' ], $errors );
+ $this->assertSame( [ 'archive.peteradamsphoto.com: OWA-gone is not in the list fetched from OWA, so its Observation Profile was not changed.' ], $errors );
}
public function testNetworkModeNeedsAMultisiteAndBothSettings(): void {
@@ -236,7 +253,7 @@ public function testAConfiguredSitesPageShowsItsWebsiteIdReadOnly(): void {
$this->assertTrue( $fields['owaEndpoint']['managed'] );
$this->assertFalse( $fields['owaEndpoint']['readonly'], 'Managed and not read-only: hidden.' );
$this->assertSame( 'OWA-archive', $payload['values']['siteId'] );
- $this->assertStringContainsString( 'configures this site\'s Profile', $payload['network']['note'] );
+ $this->assertStringContainsString( 'configures this site\'s Observation Profile', $payload['network']['note'] );
}
private function pluginListing( array $rows ): \owaWp_plugin {
@@ -288,7 +305,7 @@ public function testAnUnassignedSiteSaysSo(): void {
$plugin = $this->pluginListing( self::rows( 'https://photopress.io' ) );
$this->assertSame( [], $plugin->getSitesFromOwa() );
- $this->assertSame( 'The network administrator has not assigned this site an OWA Profile, so it is not tracked.', $plugin->getSitesNotice() );
+ $this->assertSame( 'The network administrator has not assigned this site an Observation Profile, so it is not tracked.', $plugin->getSitesNotice() );
}
// ---- the network settings ------------------------------------------------
@@ -349,7 +366,7 @@ public function getLastSitesError(): string { return ''; }
private function credentials( array $more = [] ): array {
- return [ 'owaEndpoint' => 'https://owa.example.com/', 'appPassword' => self::TOKEN, 'assignProfiles' => true ] + $more;
+ return $more + [ 'owaEndpoint' => 'https://owa.example.com/', 'appPassword' => self::TOKEN, 'assignProfiles' => true ];
}
public function testTurningOnNetworkConfigurationStartsEachSiteOnItsExactMatch(): void {
@@ -386,7 +403,7 @@ public function testAProfileNotInTheListIsRefusedAndTheRestSaved(): void {
[ , $errors ] = network::save( $this->credentials( [ 'assignedSiteIds' => [ 1 => 'OWA-gone', 3 => 'OWA-www' ] ] ), $this->networkPlugin() );
$this->assertSame( [ 1 => 'OWA-main', 3 => 'OWA-www' ], $this->network['assignedSiteIds'] );
- $this->assertSame( [ 'photopress.io: OWA-gone is not in the list fetched from OWA, so its Profile was not changed.' ], $errors['assignedSiteIds'] );
+ $this->assertSame( [ 'photopress.io: OWA-gone is not in the list fetched from OWA, so its Observation Profile was not changed.' ], $errors['assignedSiteIds'] );
}
public function testOpeningThePageFetchesTheProfilesAndKeepsTheMapping(): void {
@@ -416,8 +433,8 @@ public function testOpeningThePageSaysWhatIsNewAndWhatIsGone(): void {
$this->assertSame( [ 'OWA-arch', 'OWA-www' ], $this->network['newProfileIds'] );
$this->assertSame( [
- 'New in OWA since the last visit: OWA-arch, OWA-www.',
- 'No longer in OWA: OWA-old.',
+ 'New Observation Profiles in OWA since the last visit: OWA-arch, OWA-www.',
+ 'Observation Profiles no longer in OWA: OWA-old.',
'New sites in the network, marked below: archive.peteradamsphoto.com.',
], $result['info'] );
}
@@ -451,7 +468,7 @@ public function getLastSitesError(): string { return 'Could not resolve host'; }
$result = network::load( $down );
- $this->assertSame( 'Could not load the list of websites from OWA: Could not resolve host. Showing the Profiles fetched 2026-10-14 17:46 UTC.', $result['error'] );
+ $this->assertSame( 'Could not load the list of Observation Profiles from OWA: Could not resolve host. Showing the Observation Profiles fetched 2026-10-14 17:46 UTC.', $result['error'] );
$this->assertSame( $before, $this->network, 'The cached Profiles stay.' );
}
@@ -473,6 +490,117 @@ public function testSavingClearsTheNewMarks(): void {
$this->assertSame( [ 1 => 'OWA-main', 3 => 'OWA-www' ], $this->network['assignedSiteIds'] );
}
+ // ---- the per-site switches (network-configured mode) ------------------------
+
+ public function testASiteTheNetworkSwitchedOffIsNotTracked(): void {
+
+ $this->networkMode( [ 'assignProfiles' => true, 'assignedSiteIds' => [ 3 => 'OWA-archive' ], 'disabledSiteIds' => [ 3 ] ] );
+ $this->onBlog( 3, 'archive.peteradamsphoto.com' );
+
+ Functions\when( 'get_option' )->justReturn( [ 'enable' => 1 ] );
+ \Brain\Monkey\Actions\expectAdded( 'wp_head' )->never();
+
+ $plugin = new \owaWp_plugin();
+ $plugin->_init();
+
+ $this->assertSame( 0, $plugin->getOption( 'enable' ), 'The site\'s own Enable OWA does not count.' );
+ $this->assertStringContainsString( 'has tracking on this site turned off', $plugin->getNetworkNote() );
+ }
+
+ public function testASiteTheNetworkSwitchedOnIsTrackedWhateverItsOwnToggle(): void {
+
+ $this->networkMode( [ 'assignProfiles' => true, 'assignedSiteIds' => [ 3 => 'OWA-archive' ], 'disabledSiteIds' => [ 4 ] ] );
+ $this->onBlog( 3, 'archive.peteradamsphoto.com' );
+
+ $this->assertSame( 1, $this->initedPlugin( [ 'enable' => 0 ] )->getOption( 'enable' ) );
+ }
+
+ public function testSwitchesApplyOnlyWhenTheNetworkConfiguresProfiles(): void {
+
+ $this->networkMode( [ 'disabledSiteIds' => [ 3 ] ] );
+ $this->onBlog( 3, 'archive.peteradamsphoto.com' );
+
+ $this->assertSame( 1, $this->initedPlugin( [ 'enable' => 1 ] )->getOption( 'enable' ), 'Site-choice mode: the site\'s own toggle.' );
+
+ $options = ( new \owaWp_plugin() )->registerOptions();
+ $this->assertArrayNotHasKey( 'managed', $options['enable']['field'] );
+
+ $this->network['assignProfiles'] = true;
+ $this->assertTrue( ( new \owaWp_plugin() )->registerOptions()['enable']['field']['managed'], 'Network-configured: the network\'s switch.' );
+ }
+
+ public function testTheSwitchesAreSavedForTheNetworksSitesOnly(): void {
+
+ $this->stubBlogs();
+ $this->networkMode( [ 'assignProfiles' => true, 'sites' => network::cacheRows( $this->networkPlugin()->listSitesWithNetworkCredentials() ), 'disabledSiteIds' => [ 1 ] ] );
+
+ network::save( $this->credentials( [ 'enabledSites' => [ 1 => true, 3 => false, 99 => false ] ] ), $this->networkPlugin() );
+ $this->assertSame( [ 3 ], $this->network['disabledSiteIds'] );
+
+ network::save( $this->credentials(), $this->networkPlugin() );
+ $this->assertSame( [ 3 ], $this->network['disabledSiteIds'], 'Left out: kept.' );
+ }
+
+ public function testANewSiteStartsSwitchedOn(): void {
+
+ $this->stubBlogs();
+ $this->networkMode( [ 'assignProfiles' => true, 'disabledSiteIds' => [ 1 ] ] );
+
+ $blogs = array_column( rest::networkPayload()['blogs'], null, 'host' );
+
+ $this->assertFalse( $blogs['photopress.io']['enabled'] );
+ $this->assertTrue( $blogs['archive.peteradamsphoto.com']['enabled'] );
+ }
+
+ // ---- purging page caches when a site's tag changes ---------------------------
+
+ private function configured( array $more = [] ): void {
+
+ $this->stubBlogs();
+ $this->networkMode( $more + [
+ 'assignProfiles' => true,
+ 'sites' => network::cacheRows( $this->networkPlugin()->listSitesWithNetworkCredentials() ),
+ 'assignedSiteIds' => [ 1 => 'OWA-main', 3 => 'OWA-arch' ],
+ ] );
+ }
+
+ public function testSwitchingASiteOffPurgesThatSiteInItsOwnContext(): void {
+
+ $this->configured();
+
+ network::save( $this->credentials( [ 'enabledSites' => [ 1 => true, 3 => false ] ] ), $this->networkPlugin() );
+
+ $this->assertSame( [ [ 3, 3 ] ], $this->purged, 'Fired once, for blog 3, while switched to blog 3.' );
+ $this->assertSame( 1, $this->currentBlog, 'Switched back.' );
+ }
+
+ public function testAssigningAnotherProfilePurgesThatSite(): void {
+
+ $this->configured();
+
+ network::save( $this->credentials( [ 'assignedSiteIds' => [ 1 => 'OWA-main', 3 => 'OWA-www' ] ] ), $this->networkPlugin() );
+
+ $this->assertSame( [ [ 3, 3 ] ], $this->purged );
+ }
+
+ public function testANewServerPurgesEverySite(): void {
+
+ $this->configured();
+
+ network::save( $this->credentials( [ 'owaEndpoint' => 'https://owa2.example.com/' ] ), $this->networkPlugin() );
+
+ $this->assertSame( [ [ 1, 1 ], [ 3, 3 ] ], $this->purged );
+ }
+
+ public function testASaveThatChangesNoTagPurgesNothing(): void {
+
+ $this->configured();
+
+ network::save( $this->credentials( [ 'assignedSiteIds' => [ 1 => 'OWA-main', 3 => 'OWA-arch' ], 'enabledSites' => [ 1 => true, 3 => true ] ] ), $this->networkPlugin() );
+
+ $this->assertSame( [], $this->purged );
+ }
+
// ---- what the network page is given ----------------------------------------
public function testTheMappingStartsOnTheExactMatchAndMarksWhatIsNew(): void {
@@ -494,11 +622,9 @@ public function testTheMappingStartsOnTheExactMatchAndMarksWhatIsNew(): void {
$this->assertStringNotContainsString( self::TOKEN, json_encode( $payload ), 'The password never leaves the server.' );
$this->assertSame( 'OWA-gone', $blogs['photopress.io']['saved'] );
- $this->assertSame( 'The saved Profile is no longer in the list from OWA', $blogs['photopress.io']['status'] );
$this->assertNull( $blogs['archive.peteradamsphoto.com']['saved'], 'New to the network: not saved.' );
$this->assertSame( 'OWA-arch', $blogs['archive.peteradamsphoto.com']['selected'], 'Starts on its exact match.' );
- $this->assertSame( 'New site: starts on its exact match, saved when you save', $blogs['archive.peteradamsphoto.com']['status'] );
$this->assertSame( 'peteradamsphoto.com', $blogs['archive.peteradamsphoto.com']['baseDomain'] );
$this->assertTrue( $profiles['OWA-www']['new'] );
@@ -510,7 +636,7 @@ public function testWithoutConfigurationTheTableShowsEachSitesOwnChoice(): void
$this->stubBlogs();
$this->networkMode();
- Functions\when( 'get_blog_option' )->alias( fn( $blog_id ) => $blog_id === 3 ? [ 'siteId' => 'OWA-own' ] : false );
+ $this->ownOptions = [ 3 => [ 'siteId' => 'OWA-own' ] ];
$blogs = array_column( rest::networkPayload()['blogs'], null, 'host' );
diff --git a/tests/OwaV2Test.php b/tests/OwaV2Test.php
index 604aeab..bd2bd2e 100644
--- a/tests/OwaV2Test.php
+++ b/tests/OwaV2Test.php
@@ -142,7 +142,7 @@ public function testAnHttpEndpointIsReportedUnderThePicker(): void {
}
$this->assertSame( [ 'OWA-9b62dc7ce78a6934' ], array_keys( $sites ), 'The saved ID stays selectable.' );
- $this->assertSame( 'Could not load the list of websites from OWA: Application password not sent: instance_url is not https', $plugin->getSitesNotice() );
+ $this->assertSame( 'Could not load the list of Observation Profiles from OWA: Application password not sent: instance_url is not https', $plugin->getSitesNotice() );
}
public function testTheSdkConfigCanBeFiltered(): void {
@@ -157,6 +157,18 @@ public function testTheSdkConfigCanBeFiltered(): void {
$this->assertTrue( $plugin->owaSdk->createSites()->getSetting( 'allow_insecure_app_password' ) );
}
+ public function testTheWebsiteIdListWorksWithTrackingOff(): void {
+
+ Functions\when( 'get_option' )->justReturn( [
+ 'enable' => 0, 'owaVersion' => '2', 'owaEndpoint' => 'https://owa.example.com/', 'appPassword' => self::TOKEN,
+ ] );
+
+ $plugin = $this->makePlugin();
+ $plugin->_init();
+
+ $this->assertInstanceOf( \OwaSdk\sdk::class, $plugin->owaSdk, 'The settings page can still list the Website IDs.' );
+ }
+
public function testTheLegacyKeysAreNotEnoughIn2x(): void {
$this->assertFalse( $this->v2( [ 'apiKey' => 'legacy-key', 'authKey' => 'legacy-auth' ] )->isOwaSdkReady() );
diff --git a/tests/PluginTest.php b/tests/PluginTest.php
index 8b04675..b7bb077 100644
--- a/tests/PluginTest.php
+++ b/tests/PluginTest.php
@@ -75,4 +75,46 @@ public function testNagDefaultsToAWarning(): void {
$this->assertStringContainsString( '', $out );
$this->assertStringContainsString( '', $out );
}
+
+ public function testSetupNoticesAreWordedForTheVersionAsked(): void {
+
+ $plugin = $this->makePlugin( [ 'owaVersion' => '1' ] );
+
+ $this->assertSame( [
+ 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.',
+ 'Open Web Analytics requires that you select a Website ID before tracking can begin.',
+ ], $plugin->setupNotices() );
+
+ $this->assertSame( [
+ 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.',
+ 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.',
+ ], $plugin->setupNotices( 2 ) );
+ }
+
+ public function testSetupNoticesGoOnceTheSettingsAreSaved(): void {
+
+ $plugin = $this->makePlugin( [ 'owaVersion' => '2', 'owaEndpoint' => 'https://owa.example.com/', 'appPassword' => 'owa_app_x', 'siteId' => 'abc123' ] );
+
+ $this->assertSame( [], $plugin->setupNotices() );
+ $this->assertSame( [ 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.' ], $plugin->setupNotices( 1 ) );
+ }
+
+ public function testTheSettingsScreenLeavesSetupNoticesToThePage(): void {
+
+ $plugin = $this->makePlugin();
+ $plugin->addNag( 'Broken.', 'notice-error' );
+
+ Functions\when( 'get_current_screen' )->justReturn( (object) [ 'id' => 'toplevel_page_owa-wordpress' ] );
+ ob_start();
+ $plugin->showNag();
+ $settings = ob_get_clean();
+
+ Functions\when( 'get_current_screen' )->justReturn( (object) [ 'id' => 'dashboard' ] );
+ ob_start();
+ $plugin->showNag();
+ $dashboard = ob_get_clean();
+
+ $this->assertSame( '', $settings );
+ $this->assertStringContainsString( 'select a Website ID before tracking can begin', $dashboard );
+ }
}
diff --git a/tests/RestTest.php b/tests/RestTest.php
index 34f3fe6..ef5cc25 100644
--- a/tests/RestTest.php
+++ b/tests/RestTest.php
@@ -76,7 +76,7 @@ public function testA2xWebsiteIdMustBeABundleFileName(): void {
[ , $errors ] = rest::sanitizeSettings( $this->schema(), [ 'owaVersion' => '2', 'siteId' => '../owa-config' ], [] );
- $this->assertSame( 'OWA 2.x cannot publish a tracking bundle for this Website ID.', $errors['siteId'] );
+ $this->assertSame( 'OWA 2.x cannot publish a tracking bundle for this Observation Profile.', $errors['siteId'] );
}
public function testSecretsAreKeptUnlessReplacedOrCleared(): void {
@@ -120,6 +120,44 @@ public function testASaveStoresAndReturnsThePageAgain(): void {
$this->assertSame( 'https://owa.example.com/index.php?owa_do=base.sitesInvocation&owa_siteId=OWA-1', $result['tagSettingsUrl'] );
}
+ // ---- page caches ------------------------------------------------------------
+
+ public function testProxyCachePurgeIsToldToPurgeTheWholeSite(): void {
+
+ \Brain\Monkey\Filters\expectAdded( 'varnish_http_purge_events' )->once();
+ \Brain\Monkey\Filters\expectAdded( 'varnish_http_purge_events_full' )->once();
+
+ \owaWp\cache::register();
+
+ $this->assertSame( [ 'save_post', \owaWp\cache::TAG_CHANGED ], \owaWp\cache::addPurgeEvent( [ 'save_post' ] ) );
+ $this->assertSame( [ \owaWp\cache::TAG_CHANGED ], \owaWp\cache::addPurgeEvent( [ \owaWp\cache::TAG_CHANGED ] ), 'Added once.' );
+ }
+
+ public function testASiteSavePurgesWhenItsTagChanges(): void {
+
+ Functions\when( 'get_current_blog_id' )->justReturn( 1 );
+ $this->stored = [ 'enable' => 1, 'owaEndpoint' => 'https://owa.example.com/', 'siteId' => 'abc123' ];
+
+ $plugin = new \owaWp_plugin();
+ $plugin->reloadOptions();
+
+ \Brain\Monkey\Actions\expectDone( \owaWp\cache::TAG_CHANGED )->once()->with( 1 );
+
+ rest::saveSettings( $plugin, [ 'enable' => false ] );
+ }
+
+ public function testASiteSaveThatLeavesTheTagAlonePurgesNothing(): void {
+
+ $this->stored = [ 'enable' => 1, 'owaEndpoint' => 'https://owa.example.com/', 'siteId' => 'abc123', 'trackNamedUsers' => 1 ];
+
+ $plugin = new \owaWp_plugin();
+ $plugin->reloadOptions();
+
+ \Brain\Monkey\Actions\expectDone( \owaWp\cache::TAG_CHANGED )->never();
+
+ rest::saveSettings( $plugin, [ 'enable' => true, 'trackNamedUsers' => false ] );
+ }
+
// ---- what the page is given -------------------------------------------------
public function testSecretsNeverLeaveTheServer(): void {
diff --git a/tests/TestCase.php b/tests/TestCase.php
index d435adf..873a4de 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -35,6 +35,7 @@ protected function setUp(): void {
'is_preview' => false,
'is_customize_preview' => false,
'is_multisite' => false,
+ 'get_current_screen' => fn() => null,
] );
require_once dirname( __DIR__ ) . '/owa-plugin.php';
diff --git a/tests/e2e/multisite.spec.js b/tests/e2e/multisite.spec.js
index 0f3b92b..13fb9a5 100644
--- a/tests/e2e/multisite.spec.js
+++ b/tests/e2e/multisite.spec.js
@@ -7,8 +7,10 @@ const { env, login, notice, shot, owaSiteId, owaCli, wp } = require( './helpers'
const NETWORK_PAGE = 'http://wp.test/wp-admin/network/admin.php?page=owa-wordpress-network';
-const site = ( page, host ) => page.getByLabel( `Website ID for ${ host }`, { exact: true } );
-const status = ( page, host ) => page.locator( 'tr', { has: site( page, host ) } ).locator( '.owa-wp-status' );
+const row = ( page, host ) => page.locator( `tr[data-host="${ host }"]` );
+const site = ( page, host ) => page.getByLabel( `Observation Profile for ${ host }`, { exact: true } );
+const edit = ( page, host ) => page.getByRole( 'button', { name: `Edit the Observation Profile for ${ host }`, exact: true } );
+const tracking = ( page, host ) => page.getByLabel( `Track ${ host }`, { exact: true } );
test( 'the network configures each site\'s Profile', async ( { page, browser }, testInfo ) => {
@@ -23,14 +25,21 @@ test( 'the network configures each site\'s Profile', async ( { page, browser },
await page.getByLabel( 'OWA Endpoint' ).fill( env( 'OWA_URL' ) );
await page.getByLabel( 'Application Password' ).fill( env( 'OWA_APP_PASSWORD' ) );
- await page.getByLabel( /Network configures each site's Profile/ ).check();
+ await page.getByLabel( /Network configures each site's Observation Profile/ ).check();
await save.click();
- // each site starts on its exact match; b.wp.test has none
+ // each site is saved on its exact match and shown as text with Edit and a switch;
+ // b.wp.test has none, so it keeps its dropdown and has no switch
await expect( notice( page, 'Settings saved.' ) ).toBeVisible();
- await expect( site( page, 'wp.test' ) ).toHaveValue( main );
- await expect( site( page, 'a.wp.test' ) ).toHaveValue( siteA );
+ await expect( row( page, 'wp.test' ) ).toContainText( main );
+ await expect( row( page, 'a.wp.test' ) ).toContainText( `Site A – https://a.wp.test (${ siteA })` );
+ await expect( site( page, 'a.wp.test' ) ).toHaveCount( 0 );
+ await expect( edit( page, 'a.wp.test' ) ).toBeVisible();
+ await expect( tracking( page, 'a.wp.test' ) ).toBeVisible();
await expect( site( page, 'b.wp.test' ) ).toHaveValue( '' );
+ await expect( site( page, 'b.wp.test' ).locator( 'option:checked' ) ).toHaveText( 'Select Profile…' );
+ await expect( edit( page, 'b.wp.test' ) ).toHaveCount( 0 );
+ await expect( tracking( page, 'b.wp.test' ) ).toHaveCount( 0 );
await shot( page, testInfo, '02-mapping-table' );
// a new Profile in OWA and a new site in the network, while the page is open
@@ -42,50 +51,78 @@ test( 'the network configures each site\'s Profile', async ( { page, browser },
// reloading the page fetches the Profiles and reads the sites together
await page.reload();
- await expect( notice( page, `New in OWA since the last visit: ${ siteB }.` ) ).toBeVisible();
+ await expect( notice( page, `New Observation Profiles in OWA since the last visit: ${ siteB }.` ) ).toBeVisible();
await expect( notice( page, 'New sites in the network, marked below: c.wp.test.' ) ).toBeVisible();
- await expect( status( page, 'c.wp.test' ) ).toHaveText( /^New site/ );
- await expect( site( page, 'b.wp.test' ).locator( `option[value="${ siteB }"]` ) ).toHaveText( / — new$/ );
+ // the new site is not saved: marked New and shown with its dropdown, without a switch
+ await expect( row( page, 'c.wp.test' ).locator( '.owa-wp-badge' ) ).toHaveText( 'New' );
+ await expect( site( page, 'c.wp.test' ) ).toBeVisible();
+ await expect( tracking( page, 'c.wp.test' ) ).toHaveCount( 0 );
await shot( page, testInfo, '03-reloaded' );
- // the admin assigns the new Profile; the row says it is not saved yet
+ // b.wp.test's dropdown lists the new Profile, marked
+ await expect( site( page, 'b.wp.test' ).locator( `option[value="${ siteB }"]` ) ).toHaveText( / — new$/ );
await site( page, 'b.wp.test' ).selectOption( siteB );
- await expect( status( page, 'b.wp.test' ) ).toHaveText( 'Changed: saved when you save' );
await shot( page, testInfo, '04-edited' );
+ // Cancel puts a row back as saved
+ await edit( page, 'wp.test' ).click();
+ await row( page, 'wp.test' ).getByRole( 'button', { name: 'Cancel' } ).click();
+ await expect( site( page, 'wp.test' ) ).toHaveCount( 0 );
+
await save.click();
await expect( notice( page, 'Settings saved.' ) ).toBeVisible();
- await expect( site( page, 'b.wp.test' ) ).toHaveValue( siteB );
- await expect( status( page, 'b.wp.test' ) ).toHaveText( 'Saved' );
+ // b.wp.test now has a saved Profile: text, Edit and its switch, on
+ await expect( row( page, 'b.wp.test' ) ).toContainText( `Site B – https://b.wp.test (${ siteB })` );
+ await expect( site( page, 'b.wp.test' ) ).toHaveCount( 0 );
+ await expect( tracking( page, 'b.wp.test' ) ).toBeChecked();
+ // c.wp.test was saved without a Profile: no longer New, still a dropdown and no switch
+ await expect( row( page, 'c.wp.test' ).locator( '.owa-wp-badge' ) ).toHaveCount( 0 );
+ await expect( site( page, 'c.wp.test' ) ).toBeVisible();
+ await expect( tracking( page, 'c.wp.test' ) ).toHaveCount( 0 );
await expect( page.getByText( ' — new' ) ).toHaveCount( 0 );
await shot( page, testInfo, '05-saved' );
+ // the network switches a.wp.test off: the row greys out and cannot be edited; it keeps its Profile
+ await expect( tracking( page, 'a.wp.test' ) ).toBeChecked();
+ await tracking( page, 'a.wp.test' ).uncheck();
+ await expect( row( page, 'a.wp.test' ) ).toHaveClass( /is-disabled/ );
+ await expect( edit( page, 'a.wp.test' ) ).toBeDisabled();
+ await save.click();
+ await expect( notice( page, 'Settings saved.' ) ).toBeVisible();
+ await expect( tracking( page, 'a.wp.test' ) ).not.toBeChecked();
+ await expect( row( page, 'a.wp.test' ) ).toHaveClass( /is-disabled/ );
+ await expect( row( page, 'a.wp.test' ) ).toContainText( `Site A – https://a.wp.test (${ siteA })` );
+ await shot( page, testInfo, '05b-site-switched-off' );
+
// the site shows what the network assigned, read-only, and none of the network's fields
await page.goto( 'http://a.wp.test/wp-admin/admin.php?page=owa-wordpress' );
- await expect( page.getByLabel( 'Website ID', { exact: true } ) ).toBeDisabled();
- await expect( page.getByLabel( 'Website ID', { exact: true } ) ).toHaveValue( siteA );
+ await expect( page.getByLabel( 'Observation Profile', { exact: true } ) ).toBeDisabled();
+ await expect( page.getByLabel( 'Observation Profile', { exact: true } ) ).toHaveValue( siteA );
+ // labelled from OWA's list, though the network has tracking off here
+ await expect( page.getByLabel( 'Observation Profile', { exact: true } ).locator( 'option:checked' ) ).toHaveText( /^Site A – https:\/\/a\.wp\.test/ );
await expect( page.getByLabel( 'OWA Endpoint' ) ).toHaveCount( 0 );
await expect( page.getByLabel( 'Application Password' ) ).toHaveCount( 0 );
await expect( page.getByLabel( '2.x (beta)' ) ).toHaveCount( 0 );
- await expect( page.getByText( 'configures this site\'s Profile' ) ).toBeVisible();
+ await expect( page.getByText( 'configures this site\'s Observation Profile and has tracking on this site turned off' ) ).toBeVisible();
+ await expect( page.getByLabel( 'Enable OWA' ) ).toHaveCount( 0 );
await shot( page, testInfo, '06-site-assigned' );
- // visitors to each site get that site's bundle
+ // visitors to b.wp.test get its bundle; a.wp.test is switched off, c.wp.test has no Profile
const visitor = await ( await browser.newContext() ).newPage();
- for ( const [ host, id ] of [ [ 'a.wp.test', siteA ], [ 'b.wp.test', siteB ] ] ) {
- const bundle = visitor.waitForResponse( ( r ) => r.url().endsWith( `/public/tracker/${ id }.js` ) );
+ const bundle = visitor.waitForResponse( ( r ) => r.url().endsWith( `/public/tracker/${ siteB }.js` ) );
+ await visitor.goto( 'http://b.wp.test/' );
+ expect( ( await bundle ).status(), 'b.wp.test' ).toBe( 200 );
+ for ( const host of [ 'a.wp.test', 'c.wp.test' ] ) {
await visitor.goto( `http://${ host }/` );
- expect( ( await bundle ).status(), host ).toBe( 200 );
+ expect( await visitor.content(), host ).not.toContain( 'Open Web Analytics Tracker' );
}
- await visitor.goto( 'http://c.wp.test/' );
- expect( await visitor.content(), 'c.wp.test has no Profile' ).not.toContain( 'Open Web Analytics Tracker' );
} );
test( 'each site picks from the Profiles sharing its domain', async ( { page }, testInfo ) => {
await login( page );
await page.goto( NETWORK_PAGE );
- await page.getByLabel( /Network configures each site's Profile/ ).uncheck();
+ await page.getByLabel( /Network configures each site's Observation Profile/ ).uncheck();
await expect( page.getByText( 'Save to switch.' ) ).toBeVisible();
await page.getByRole( 'button', { name: 'Save Changes' } ).click();
await expect( notice( page, 'Settings saved.' ) ).toBeVisible();
@@ -93,7 +130,7 @@ test( 'each site picks from the Profiles sharing its domain', async ( { page },
await shot( page, testInfo, '07-network-site-choice' );
await page.goto( 'http://b.wp.test/wp-admin/admin.php?page=owa-wordpress' );
- const picker = page.getByLabel( 'Website ID', { exact: true } );
+ const picker = page.getByLabel( 'Observation Profile', { exact: true } );
await expect( picker.locator( 'option' ) ).not.toHaveCount( 1 );
await expect( picker ).toBeEnabled();
@@ -101,6 +138,6 @@ test( 'each site picks from the Profiles sharing its domain', async ( { page },
const labels = await picker.locator( 'option' ).allTextContents();
expect( labels.some( ( v ) => v.includes( 'https://a.wp.test' ) ) ).toBe( true );
expect( labels.some( ( v ) => v.includes( 'other.example' ) ) ).toBe( false );
- await expect( page.getByText( 'The list shows the Profiles that share this site\'s domain.' ) ).toBeVisible();
+ await expect( page.getByText( 'The list shows the Observation Profiles that share this site\'s domain.' ) ).toBeVisible();
await shot( page, testInfo, '08-site-choice' );
} );
diff --git a/tests/e2e/single.spec.js b/tests/e2e/single.spec.js
index ea896b9..923dfbd 100644
--- a/tests/e2e/single.spec.js
+++ b/tests/e2e/single.spec.js
@@ -17,6 +17,11 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle'
await expect( field( '1.x' ) ).toBeChecked();
await expect( field( 'API Key' ) ).toBeVisible();
await expect( field( 'Application Password' ) ).toHaveCount( 0 );
+ await expect( field( 'Website ID' ) ).toBeVisible();
+ // what is left to set up, worded for 1.x, shown by the page and not again by WordPress
+ await expect( notice( page, 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.' ) ).toBeVisible();
+ await expect( notice( page, 'Open Web Analytics requires that you select a Website ID before tracking can begin.' ) ).toBeVisible();
+ await expect( page.locator( 'div.notice-warning' ) ).toHaveCount( 0 );
await shot( page, testInfo, '01-settings-1x' );
await tab( 'Tracking' ).click();
@@ -29,6 +34,13 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle'
await expect( field( 'API Key' ) ).toHaveCount( 0 );
await expect( field( 'Application Password' ) ).toBeVisible();
await expect( tab( 'Advanced' ) ).toHaveCount( 0 );
+ // the same field is the Observation Profile in 2.x
+ await expect( field( 'Website ID' ) ).toHaveCount( 0 );
+ await expect( field( 'Observation Profile' ).locator( 'option:checked' ) ).toHaveText( /^(Loading|Select Profile)…$/ );
+ // the notices switch to 2.x wording with it
+ await expect( notice( page, 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.' ) ).toBeVisible();
+ await expect( notice( page, 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' ) ).toBeVisible();
+ await expect( notice( page, 'API Key and Secret Auth Key' ) ).toHaveCount( 0 );
await shot( page, testInfo, '03-switched-to-2x' );
await tab( 'Tracking' ).click();
@@ -50,18 +62,33 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle'
await save.click();
await expect( notice( page, 'Settings saved.' ) ).toBeVisible();
await expect( page.getByText( 'Saved. Enter a new one to replace it.' ) ).toBeVisible();
+ // saved credentials: that notice goes without a reload; choosing an Observation Profile is left
+ await expect( notice( page, 'to load the list of Observation Profiles from OWA' ) ).toHaveCount( 0 );
+ await expect( notice( page, 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' ) ).toBeVisible();
// the list comes from OWA with the application password
- await expect( field( 'Website ID' ).locator( `option[value="${ siteId }"]` ) ).toHaveCount( 1 );
- await field( 'Website ID' ).selectOption( siteId );
+ await expect( field( 'Observation Profile' ).locator( `option[value="${ siteId }"]` ) ).toHaveCount( 1 );
+ await field( 'Observation Profile' ).selectOption( siteId );
+
+ // chosen but not saved: no link to OWA's tracking tag screen yet
+ const tagLink = page.getByRole( 'link', { name: /Configure this Observation Profile's tracking tag in OWA/ } );
+ await expect( tagLink ).toHaveCount( 0 );
await save.click();
await expect( notice( page, 'Settings saved.' ) ).toBeVisible();
- await expect( page.getByRole( 'link', { name: /Configure this Website ID's tracking tag in OWA/ } ) ).toBeVisible();
+ await expect( tagLink ).toBeVisible();
+ await expect( notice( page, 'before tracking can begin' ) ).toHaveCount( 0 );
await shot( page, testInfo, '06-2x-saved' );
+ // choosing another Observation Profile hides it again until that is saved
+ await field( 'Observation Profile' ).selectOption( '' );
+ await expect( tagLink ).toHaveCount( 0 );
+ await field( 'Observation Profile' ).selectOption( siteId );
+ await expect( tagLink ).toBeVisible();
+
// the page loads saved, and the password is not in it
await page.reload();
- await expect( field( 'Website ID' ) ).toHaveValue( siteId );
+ await expect( field( 'Observation Profile' ) ).toHaveValue( siteId );
+ await expect( field( 'Observation Profile' ).locator( 'option:checked' ) ).not.toHaveText( /not in the list from OWA/ );
expect( await page.content() ).not.toContain( env( 'OWA_APP_PASSWORD' ) );
// a visitor gets the bundle tag, and the bundle loads
diff --git a/tests/js/fields.test.js b/tests/js/fields.test.js
index 8664f68..5f7bfdf 100644
--- a/tests/js/fields.test.js
+++ b/tests/js/fields.test.js
@@ -1,5 +1,5 @@
import { test, expect } from 'vitest';
-import { isVisible, visibleFields, visibleSections } from '../../js/lib/fields';
+import { fieldText, isVisible, setupNotices, showTagSettingsLink, visibleFields, visibleSections } from '../../js/lib/fields';
const FIELDS = [
{ key: 'owaVersion', section: 'general', version: null },
@@ -31,3 +31,29 @@ test( 'a field the network manages is hidden unless shown read-only', () => {
expect( isVisible( { key: 'owaEndpoint', managed: true, readonly: false }, '2' ) ).toBe( false );
expect( isVisible( { key: 'siteId', managed: true, readonly: true }, '2' ) ).toBe( true );
} );
+
+test( 'the link to OWA\'s tracking tag screen shows only for the saved Website ID', () => {
+ const saved = { owaVersion: '2', owaEndpoint: 'https://owa.example.com/', siteId: 'OWA-1', trackNamedUsers: true };
+ const payload = { tagSettingsUrl: 'https://owa.example.com/index.php?owa_do=base.sitesInvocation&owa_siteId=OWA-1', values: saved };
+
+ expect( showTagSettingsLink( payload, saved ) ).toBe( true );
+ expect( showTagSettingsLink( payload, { ...saved, trackNamedUsers: false } ) ).toBe( true );
+ expect( showTagSettingsLink( payload, { ...saved, siteId: 'OWA-2' } ) ).toBe( false );
+ expect( showTagSettingsLink( payload, { ...saved, owaVersion: '1' } ) ).toBe( false );
+ expect( showTagSettingsLink( payload, { ...saved, owaEndpoint: 'https://other.example.com/' } ) ).toBe( false );
+ expect( showTagSettingsLink( { ...payload, tagSettingsUrl: '' }, saved ) ).toBe( false );
+} );
+
+test( 'a field takes its 2.x wording only while 2.x is chosen', () => {
+ const siteId = { title: 'Website ID', description: 'Pick a website.', v2: { title: 'Observation Profile', description: 'Pick a Profile.' } };
+ expect( fieldText( siteId, '1' ) ).toEqual( { title: 'Website ID', description: 'Pick a website.' } );
+ expect( fieldText( siteId, '2' ) ).toEqual( { title: 'Observation Profile', description: 'Pick a Profile.' } );
+ expect( fieldText( { title: 'OWA Endpoint', description: 'The URL.', v2: [] }, '2' ) ).toEqual( { title: 'OWA Endpoint', description: 'The URL.' } );
+} );
+
+test( 'the setup notices follow the version chosen on the page, and go while tracking is off', () => {
+ const payload = { notices: { 1: [ 'Enter your API Key.' ], 2: [ 'Enter an Application Password.' ] } };
+ expect( setupNotices( payload, { enable: true, owaVersion: '1' } ) ).toEqual( [ 'Enter your API Key.' ] );
+ expect( setupNotices( payload, { enable: true, owaVersion: '2' } ) ).toEqual( [ 'Enter an Application Password.' ] );
+ expect( setupNotices( payload, { enable: false, owaVersion: '2' } ) ).toEqual( [] );
+} );
diff --git a/tests/js/mapping.test.js b/tests/js/mapping.test.js
index e4432b8..dba9bdb 100644
--- a/tests/js/mapping.test.js
+++ b/tests/js/mapping.test.js
@@ -1,5 +1,5 @@
import { test, expect } from 'vitest';
-import { CHANGED, initialMapping, mappingPayload, profileLabel, profilesFor, rowStatus } from '../../js/lib/mapping';
+import { hasSavedProfile, initialMapping, initialSwitches, isEditing, mappingPayload, switchesPayload, profileLabel, profilesFor, savedLabel } from '../../js/lib/mapping';
const PROFILES = [
{ id: 'OWA-io', label: 'PhotoPress – https://photopress.io (OWA-io)', domain: 'https://photopress.io', baseDomain: 'photopress.io', new: false },
@@ -17,14 +17,23 @@ test( 'a site lists the Profiles of its base domain first', () => {
expect( profilesFor( PROFILES, 'photopress.io' ).map( ( p ) => p.id ) ).toEqual( [ 'OWA-io', 'OWA-arch', 'OWA-www' ] );
} );
-test( 'a row says "changed" once it differs from what was loaded', () => {
- const saved = { id: 1, saved: 'OWA-io', selected: 'OWA-io', status: 'Saved' };
- expect( rowStatus( saved, 'OWA-io' ) ).toBe( 'Saved' );
- expect( rowStatus( saved, '' ) ).toBe( CHANGED );
+test( 'a saved row shows its Profile as text', () => {
+ expect( savedLabel( PROFILES, 'OWA-arch' ) ).toBe( PROFILES[ 1 ].label );
+ expect( savedLabel( PROFILES, '' ) ).toBe( 'No Observation Profile' );
+ expect( savedLabel( PROFILES, 'OWA-gone' ) ).toBe( 'OWA-gone (not in the list from OWA)' );
+} );
+
+test( 'a row shows its dropdown until a Profile is saved for it, or after Edit', () => {
+ expect( isEditing( { id: 3, saved: null }, {} ) ).toBe( true );
+ expect( isEditing( { id: 3, saved: '' }, {} ) ).toBe( true );
+ expect( isEditing( { id: 1, saved: 'OWA-io' }, {} ) ).toBe( false );
+ expect( isEditing( { id: 1, saved: 'OWA-io' }, { 1: true } ) ).toBe( true );
+} );
- const fresh = { id: 3, saved: null, selected: 'OWA-arch', status: 'New site: starts on its exact match, saved when you save' };
- expect( rowStatus( fresh, 'OWA-arch' ) ).toBe( fresh.status );
- expect( rowStatus( fresh, 'OWA-www' ) ).toBe( CHANGED );
+test( 'only a row with a saved Profile has a switch, and only those are sent', () => {
+ const blogs = [ { id: 1, saved: 'OWA-io' }, { id: 3, saved: '' }, { id: 5, saved: null } ];
+ expect( blogs.map( hasSavedProfile ) ).toEqual( [ true, false, false ] );
+ expect( switchesPayload( blogs, { 1: false, 3: true, 5: true } ) ).toEqual( { 1: false } );
} );
test( 'the selects start where the server put them and are sent by site', () => {
@@ -33,3 +42,7 @@ test( 'the selects start where the server put them and are sent by site', () =>
expect( mapping ).toEqual( { 1: 'OWA-io', 3: '' } );
expect( mappingPayload( { ...mapping, 3: 'OWA-arch' } ) ).toEqual( { 1: 'OWA-io', 3: 'OWA-arch' } );
} );
+
+test( 'the switches start where they were saved', () => {
+ expect( initialSwitches( [ { id: 1, enabled: true }, { id: 3, enabled: false }, { id: 5, own: '' } ] ) ).toEqual( { 1: true, 3: false } );
+} );