- Introduced a new specification for adding an optional contact email field to events, including front-end output and compatibility testing. - Added a comprehensive design document for multi-day event occurrences, detailing the new occurrence data model, validation, normalization, and calendar architecture. - Created specifications for ACF field-key switching compatibility and local ACF override compatibility to ensure seamless integration with existing themes. - Implemented a desktop calendar view toggle design, allowing users to switch between calendar and list views, with persistent preferences and responsive behavior.
29 KiB
Desktop Calendar View Toggle Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add an accessible desktop Calendar/List switcher that remembers the visitor's preference while keeping mobile permanently in List view.
Architecture: Keep the existing grid and agenda rendered from one occurrence collection. Add semantic template hooks and a dependency-free browser controller that applies one localStorage preference to every calendar instance, while CSS preserves the current no-JavaScript responsive fallback.
Tech Stack: WordPress/PHP templates and enqueue APIs, CSS, browser JavaScript, Node's built-in test runner, and the plugin's custom PHP test harness.
File Map
- Modify
templates/calendar.php: render the switcher and stable hooks for each view. - Modify
assets/css/aa-events.css: style the utility row, switcher, desktop List state, shared agenda cards, and forced mobile state. - Modify
templates/aa-events.css: remain byte-identical to the frontend stylesheet. - Create
assets/js/aa-events-calendar-view.js: own preference persistence, multi-instance synchronization, accessible state, and breakpoint changes. - Create
tests/test-calendar-view.js: exercise the browser controller with dependency-free DOM/storage/media-query doubles. - Modify
tests/test-calendar-render.php: verify rendered switcher semantics and instance-safe hooks. - Modify
tests/test-calendar-css.php: verify view-switcher and responsive contracts. - Modify
tests/bootstrap.php: capture frontend asset enqueues and align the test version constant with the plugin header. - Create
tests/test-frontend-assets.php: verify the frontend controller is enqueued. - Modify
tests/run.php: include the frontend asset test. - Modify
includes/class-aa-events.php: enqueue the new frontend controller. - Modify
aa-events.php: align the asset version constant with the existing1.2.1plugin header. - Modify
tests/test-admin-consumers.php: align its expected release version with1.2.1. - Modify
README.md: document the switcher, persistence, mobile behavior, and override requirements.
The current workspace is not a Git worktree. Each commit step below is retained for use when the plugin is placed in Git, but must be reported as skipped in this workspace rather than treated as a test failure.
Task 1: Render the Accessible View Switcher
Files:
-
Modify:
tests/test-calendar-render.php -
Modify:
templates/calendar.php -
Step 1: Add failing rendered-markup assertions
After $xpath is created in tests/test-calendar-render.php, add:
$calendar_instance = $xpath->query( '//*[@data-aa-events-calendar]' )->item( 0 );
$switcher = $xpath->query( '//*[@data-aa-events-view-switcher and @role="group"]' )->item( 0 );
$view_buttons = $xpath->query( '//*[@data-aa-events-view-switcher]//button[@data-aa-events-view-button]' );
$calendar_panel = $xpath->query( '//*[@data-aa-events-view-panel="calendar"]' )->item( 0 );
$list_panel = $xpath->query( '//*[@data-aa-events-view-panel="list"]' )->item( 0 );
aa_events_assert_true( $calendar_instance instanceof DOMElement, 'Rendered calendar exposes an instance hook.' );
aa_events_assert_true( $switcher instanceof DOMElement, 'Rendered calendar exposes a grouped view switcher.' );
aa_events_assert_same( 'Calendar view', $switcher instanceof DOMElement ? $switcher->getAttribute( 'aria-label' ) : null, 'View switcher has an accessible name.' );
aa_events_assert_same( 2, $view_buttons->length, 'View switcher contains exactly two buttons.' );
aa_events_assert_same( 'calendar', $view_buttons->item( 0 ) instanceof DOMElement ? $view_buttons->item( 0 )->getAttribute( 'data-aa-events-view-button' ) : null, 'Calendar control is first.' );
aa_events_assert_same( 'true', $view_buttons->item( 0 ) instanceof DOMElement ? $view_buttons->item( 0 )->getAttribute( 'aria-pressed' ) : null, 'Calendar is the initial desktop selection.' );
aa_events_assert_same( 'list', $view_buttons->item( 1 ) instanceof DOMElement ? $view_buttons->item( 1 )->getAttribute( 'data-aa-events-view-button' ) : null, 'List control is second.' );
aa_events_assert_same( 'false', $view_buttons->item( 1 ) instanceof DOMElement ? $view_buttons->item( 1 )->getAttribute( 'aria-pressed' ) : null, 'List is initially unselected.' );
aa_events_assert_true( $calendar_panel instanceof DOMElement, 'Calendar grid exposes its view-panel hook.' );
aa_events_assert_true( $list_panel instanceof DOMElement, 'Agenda exposes its view-panel hook.' );
After $multi_xpath is created, add:
aa_events_assert_same( 2, $multi_xpath->query( '//*[@data-aa-events-calendar]' )->length, 'Two calendars expose two independent instance hooks.' );
aa_events_assert_same( 4, $multi_xpath->query( '//*[@data-aa-events-view-button]' )->length, 'Two calendars each render both view controls.' );
aa_events_assert_same( 2, $multi_xpath->query( '//*[@data-aa-events-view-panel="calendar"]' )->length, 'Two calendars each expose a grid panel.' );
aa_events_assert_same( 2, $multi_xpath->query( '//*[@data-aa-events-view-panel="list"]' )->length, 'Two calendars each expose a list panel.' );
- Step 2: Run the PHP suite and verify the new assertions fail
Run:
php tests/run.php
Expected: the new instance/switcher assertions fail. The two already-known version-consistency assertions also fail until Task 4.
- Step 3: Add the switcher and view hooks
In templates/calendar.php, change the outer and utility markup to:
<div class="aa-events-calendar-wrap container" data-aa-events-calendar>
<div class="aa-events-calendar-utility">
<div class="aa-events-calendar-today-link">
<a href="<?php echo esc_url( $calendar_url( $today ) ); ?>" class="aa-events-calendar-today-btn"><?php esc_html_e( 'Today', 'aa-events' ); ?></a>
</div>
<div class="aa-events-calendar-view-switcher" data-aa-events-view-switcher role="group" aria-label="<?php echo esc_attr( __( 'Calendar view', 'aa-events' ) ); ?>">
<button type="button" class="aa-events-calendar-view-button" data-aa-events-view-button="calendar" aria-pressed="true"><?php esc_html_e( 'Calendar', 'aa-events' ); ?></button>
<button type="button" class="aa-events-calendar-view-button" data-aa-events-view-button="list" aria-pressed="false"><?php esc_html_e( 'List', 'aa-events' ); ?></button>
</div>
</div>
Add the panel attributes to the existing view containers:
<div class="aa-events-calendar-desktop" data-aa-events-view-panel="calendar">
<section class="aa-events-calendar-agenda" data-aa-events-view-panel="list" aria-labelledby="<?php echo esc_attr( $agenda_heading_id ); ?>">
- Step 4: Run the rendered tests
Run:
php tests/run.php
Expected: all calendar rendering assertions pass; only the two pre-existing version assertions remain failing.
- Step 5: Commit the template slice when Git is available
git add templates/calendar.php tests/test-calendar-render.php
git commit -m "feat: render calendar view switcher"
In the current non-Git workspace, verify git rev-parse --show-toplevel reports no worktree and record this commit as skipped.
Task 2: Style Both Desktop Views and Preserve the Mobile Fallback
Files:
-
Modify:
tests/test-calendar-css.php -
Modify:
assets/css/aa-events.css -
Modify:
templates/aa-events.css -
Step 1: Add failing CSS contracts
Add these entries to $calendar_css_contracts in tests/test-calendar-css.php:
'/\.aa-events-calendar-utility\s*\{[^}]*(?=.*?align-items\s*:\s*center)(?=.*?display\s*:\s*flex)(?=.*?flex-wrap\s*:\s*wrap)(?=.*?justify-content\s*:\s*space-between)[^}]*\}/s' => 'Today and the view switcher share a wrapping utility row.',
'/\.aa-events-calendar-view-switcher\s*\{[^}]*(?=.*?display\s*:\s*inline-flex)[^}]*\}/s' => 'View buttons render as one segmented control.',
'/\.aa-events-calendar-view-button\s*\{[^}]*(?=.*?border\s*:)(?=.*?cursor\s*:\s*pointer)(?=.*?padding\s*:)[^}]*\}/s' => 'View buttons have a usable control surface.',
'/\.aa-events-calendar-view-button\[aria-pressed="true"\]\s*\{[^}]*(?=.*?background\s*:)(?=.*?color\s*:)(?=.*?font-weight\s*:)[^}]*\}/s' => 'The selected view has a visible state.',
'/\.aa-events-calendar-view-button:focus-visible\s*\{[^}]*(?=.*?outline\s*:)(?=.*?outline-offset\s*:)[^}]*\}/s' => 'View buttons have visible keyboard focus.',
'/\.aa-events-calendar-wrap\.is-list-view\s+\.aa-events-calendar-desktop\s*\{[^}]*display\s*:\s*none[^}]*\}/s' => 'Desktop List state hides the grid.',
'/\.aa-events-calendar-wrap\.is-list-view\s+\.aa-events-calendar-agenda\s*\{[^}]*display\s*:\s*block[^}]*\}/s' => 'Desktop List state exposes the agenda.',
'/\.aa-events-calendar-wrap\s+\[hidden\]\s*\{[^}]*display\s*:\s*none\s*!important[^}]*\}/s' => 'Inactive views are removed visually when hidden.',
'/@media\s*\(max-width\s*:\s*640px\)[\s\S]*?\.aa-events-calendar-view-switcher\s*\{[^}]*display\s*:\s*none[\s\S]*?\.aa-events-calendar-desktop\s*\{[^}]*display\s*:\s*none[\s\S]*?\.aa-events-calendar-agenda\s*\{[^}]*display\s*:\s*block/s' => 'Mobile hides the switcher and always presents the agenda.',
Keep the existing agenda-list/card/focus contracts; they will guard moving those rules outside the media query.
- Step 2: Run the CSS contract test and verify failure
Run:
php tests/run.php
Expected: the new utility, switcher, state, and hidden-selector contracts fail.
- Step 3: Implement the frontend styles
In assets/css/aa-events.css, add before .aa-events-calendar-header:
.aa-events-calendar-utility {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.625rem;
justify-content: space-between;
margin-bottom: 0.625rem;
}
.aa-events-calendar-view-switcher {
display: inline-flex;
}
.aa-events-calendar-view-button {
background: #fff;
border: 1px solid #767676;
color: #222;
cursor: pointer;
font: inherit;
margin: 0;
padding: 0.5rem 0.75rem;
}
.aa-events-calendar-view-button + .aa-events-calendar-view-button {
border-left: 0;
}
.aa-events-calendar-view-button:first-child {
border-radius: 0.25rem 0 0 0.25rem;
}
.aa-events-calendar-view-button:last-child {
border-radius: 0 0.25rem 0.25rem 0;
}
.aa-events-calendar-view-button[aria-pressed="true"] {
background: #245b87;
color: #fff;
font-weight: bold;
}
.aa-events-calendar-view-button:hover {
text-decoration: underline;
}
.aa-events-calendar-view-button:focus-visible {
outline: 3px solid currentColor;
outline-offset: 2px;
position: relative;
z-index: 1;
}
.aa-events-calendar-wrap [hidden] {
display: none !important;
}
After the base .aa-events-calendar-agenda { display: none; } rule, add:
.aa-events-calendar-wrap.is-list-view .aa-events-calendar-desktop {
display: none;
}
.aa-events-calendar-wrap.is-list-view .aa-events-calendar-agenda {
display: block;
}
Move these existing agenda rules out of @media (max-width: 640px) so the desktop List view receives the same presentation:
.aa-events-calendar-agenda-list {
list-style: none;
margin: 0;
padding: 0;
}
.aa-events-agenda-item + .aa-events-agenda-item { margin-top: 0.75rem; }
.aa-events-agenda-item > a {
border: 1px solid #aaa;
border-left: 0.25rem solid #245b87;
border-radius: 0.25rem;
display: block;
min-width: 0;
overflow-wrap: anywhere;
padding: 0.75rem;
text-decoration: underline;
}
.aa-events-agenda-item > a:hover { background: #f5f5f5; }
.aa-events-agenda-item > a:focus-visible {
outline: 3px solid currentColor;
outline-offset: 2px;
}
.aa-events-agenda-title,
.aa-events-agenda-item time {
display: block;
overflow-wrap: anywhere;
}
.aa-events-agenda-title { font-weight: bold; }
.aa-events-agenda-item .screen-reader-text {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal !important;
}
At the start of the existing 640-pixel media query, add:
.aa-events-calendar-view-switcher { display: none; }
Keep the existing mobile grid/agenda rules in their current order.
- Step 4: Synchronize the bundled CSS copy
Run:
cp assets/css/aa-events.css templates/aa-events.css
Expected: cmp -s assets/css/aa-events.css templates/aa-events.css exits with status 0.
- Step 5: Run the PHP suite
Run:
php tests/run.php
Expected: all calendar rendering and CSS assertions pass; only the two pre-existing version assertions remain failing.
- Step 6: Commit the styling slice when Git is available
git add assets/css/aa-events.css templates/aa-events.css tests/test-calendar-css.php
git commit -m "style: support desktop calendar list view"
In the current non-Git workspace, record the commit as skipped.
Task 3: Implement and Test the Persistent View Controller
Files:
-
Create:
tests/test-calendar-view.js -
Create:
assets/js/aa-events-calendar-view.js -
Step 1: Create the failing browser-controller test
Create tests/test-calendar-view.js:
'use strict';
const test = require( 'node:test' );
const assert = require( 'node:assert/strict' );
const { init } = require( '../assets/js/aa-events-calendar-view.js' );
class FakeClassList {
constructor() {
this.values = new Set();
}
add( value ) {
this.values.add( value );
}
remove( value ) {
this.values.delete( value );
}
toggle( value, force ) {
force ? this.add( value ) : this.remove( value );
}
contains( value ) {
return this.values.has( value );
}
}
class FakeElement {
constructor( attributes = {} ) {
this.attributes = { ...attributes };
this.classList = new FakeClassList();
this.hidden = false;
this.listeners = {};
}
getAttribute( name ) {
return this.attributes[ name ] ?? null;
}
setAttribute( name, value ) {
this.attributes[ name ] = String( value );
}
addEventListener( type, callback ) {
this.listeners[ type ] = callback;
}
click() {
this.listeners.click();
}
}
function makeCalendar() {
const calendarButton = new FakeElement( { 'data-aa-events-view-button': 'calendar', 'aria-pressed': 'true' } );
const listButton = new FakeElement( { 'data-aa-events-view-button': 'list', 'aria-pressed': 'false' } );
const calendarPanel = new FakeElement();
const listPanel = new FakeElement();
const root = new FakeElement();
root.querySelectorAll = ( selector ) => selector === '[data-aa-events-view-button]' ? [ calendarButton, listButton ] : [];
root.querySelector = ( selector ) => {
if ( selector === '[data-aa-events-view-panel="calendar"]' ) {
return calendarPanel;
}
if ( selector === '[data-aa-events-view-panel="list"]' ) {
return listPanel;
}
return null;
};
return { root, calendarButton, listButton, calendarPanel, listPanel };
}
function makeDocument( calendars ) {
return {
querySelectorAll( selector ) {
return selector === '[data-aa-events-calendar]' ? calendars.map( ( calendar ) => calendar.root ) : [];
},
};
}
function makeMediaQuery( matches = false ) {
return {
matches,
listener: null,
addEventListener( type, callback ) {
if ( type === 'change' ) {
this.listener = callback;
}
},
change( matchesMobile ) {
this.matches = matchesMobile;
this.listener( { matches: matchesMobile } );
},
};
}
function makeStorage( initialValue = null ) {
return {
value: initialValue,
getItem() {
return this.value;
},
setItem( key, value ) {
assert.equal( key, 'aa-events-calendar-view' );
this.value = value;
},
};
}
test( 'defaults to Calendar and synchronizes List across instances', () => {
const calendars = [ makeCalendar(), makeCalendar() ];
const storage = makeStorage();
const mediaQuery = makeMediaQuery();
init( makeDocument( calendars ), storage, mediaQuery );
assert.equal( calendars[ 0 ].calendarPanel.hidden, false );
assert.equal( calendars[ 0 ].listPanel.hidden, true );
calendars[ 0 ].listButton.click();
for ( const calendar of calendars ) {
assert.equal( calendar.root.classList.contains( 'is-list-view' ), true );
assert.equal( calendar.calendarPanel.hidden, true );
assert.equal( calendar.listPanel.hidden, false );
assert.equal( calendar.calendarButton.getAttribute( 'aria-pressed' ), 'false' );
assert.equal( calendar.listButton.getAttribute( 'aria-pressed' ), 'true' );
}
assert.equal( storage.value, 'list' );
} );
test( 'restores a valid preference and rejects an invalid value', () => {
const savedList = makeCalendar();
init( makeDocument( [ savedList ] ), makeStorage( 'list' ), makeMediaQuery() );
assert.equal( savedList.listPanel.hidden, false );
const invalid = makeCalendar();
init( makeDocument( [ invalid ] ), makeStorage( 'tiles' ), makeMediaQuery() );
assert.equal( invalid.calendarPanel.hidden, false );
assert.equal( invalid.listPanel.hidden, true );
} );
test( 'forces List on mobile without overwriting the saved desktop preference', () => {
const calendar = makeCalendar();
const storage = makeStorage( 'calendar' );
const mediaQuery = makeMediaQuery();
init( makeDocument( [ calendar ] ), storage, mediaQuery );
mediaQuery.change( true );
assert.equal( calendar.calendarPanel.hidden, true );
assert.equal( calendar.listPanel.hidden, false );
assert.equal( storage.value, 'calendar' );
mediaQuery.change( false );
assert.equal( calendar.calendarPanel.hidden, false );
assert.equal( calendar.listPanel.hidden, true );
} );
test( 'switches the current page when storage access fails', () => {
const calendar = makeCalendar();
const brokenStorage = {
getItem() {
throw new Error( 'blocked' );
},
setItem() {
throw new Error( 'blocked' );
},
};
assert.doesNotThrow( () => init( makeDocument( [ calendar ] ), brokenStorage, makeMediaQuery() ) );
calendar.listButton.click();
assert.equal( calendar.listPanel.hidden, false );
assert.equal( calendar.listButton.getAttribute( 'aria-pressed' ), 'true' );
} );
test( 'does nothing on pages without calendars and supports legacy media listeners', () => {
const mediaQuery = {
matches: false,
listener: null,
addListener( callback ) {
this.listener = callback;
},
};
assert.doesNotThrow( () => init( makeDocument( [] ), makeStorage(), mediaQuery ) );
const calendar = makeCalendar();
init( makeDocument( [ calendar ] ), makeStorage(), mediaQuery );
mediaQuery.matches = true;
mediaQuery.listener( { matches: true } );
assert.equal( calendar.listPanel.hidden, false );
} );
- Step 2: Run the Node test and verify module failure
Run:
node --test tests/test-calendar-view.js
Expected: FAIL because assets/js/aa-events-calendar-view.js does not exist.
- Step 3: Implement the controller
Create assets/js/aa-events-calendar-view.js:
( function ( globalObject, factory ) {
'use strict';
const api = factory();
if ( typeof module === 'object' && module.exports ) {
module.exports = api;
}
if ( ! globalObject || ! globalObject.document ) {
return;
}
const start = function () {
let storage = null;
try {
storage = globalObject.localStorage;
} catch ( error ) {
storage = null;
}
api.init(
globalObject.document,
storage,
globalObject.matchMedia( '(max-width: 640px)' )
);
};
if ( 'loading' === globalObject.document.readyState ) {
globalObject.document.addEventListener( 'DOMContentLoaded', start );
} else {
start();
}
}( typeof window === 'undefined' ? null : window, function () {
'use strict';
const storageKey = 'aa-events-calendar-view';
const validViews = [ 'calendar', 'list' ];
const readPreference = function ( storage ) {
try {
const storedView = storage && storage.getItem( storageKey );
return validViews.includes( storedView ) ? storedView : 'calendar';
} catch ( error ) {
return 'calendar';
}
};
const writePreference = function ( storage, view ) {
try {
if ( storage ) {
storage.setItem( storageKey, view );
}
} catch ( error ) {
// Persistence is optional; current-page switching still works.
}
};
const init = function ( documentObject, storage, mediaQuery ) {
const calendars = Array.from( documentObject.querySelectorAll( '[data-aa-events-calendar]' ) );
if ( ! calendars.length ) {
return;
}
let preference = readPreference( storage );
const applyView = function () {
const isMobile = Boolean( mediaQuery && mediaQuery.matches );
calendars.forEach( function ( calendar ) {
const buttons = Array.from( calendar.querySelectorAll( '[data-aa-events-view-button]' ) );
const calendarPanel = calendar.querySelector( '[data-aa-events-view-panel="calendar"]' );
const listPanel = calendar.querySelector( '[data-aa-events-view-panel="list"]' );
if ( ! calendarPanel || ! listPanel ) {
return;
}
buttons.forEach( function ( button ) {
const isSelected = button.getAttribute( 'data-aa-events-view-button' ) === preference;
button.setAttribute( 'aria-pressed', isSelected ? 'true' : 'false' );
} );
if ( isMobile ) {
calendar.classList.remove( 'is-list-view' );
calendarPanel.hidden = true;
listPanel.hidden = false;
return;
}
const showList = 'list' === preference;
calendar.classList.toggle( 'is-list-view', showList );
calendarPanel.hidden = showList;
listPanel.hidden = ! showList;
} );
};
const selectView = function ( view ) {
if ( ! validViews.includes( view ) ) {
return;
}
preference = view;
writePreference( storage, preference );
applyView();
};
calendars.forEach( function ( calendar ) {
Array.from( calendar.querySelectorAll( '[data-aa-events-view-button]' ) ).forEach( function ( button ) {
button.addEventListener( 'click', function () {
selectView( button.getAttribute( 'data-aa-events-view-button' ) );
} );
} );
} );
if ( mediaQuery ) {
if ( 'function' === typeof mediaQuery.addEventListener ) {
mediaQuery.addEventListener( 'change', applyView );
} else if ( 'function' === typeof mediaQuery.addListener ) {
mediaQuery.addListener( applyView );
}
}
applyView();
};
return { init };
} ) );
- Step 4: Run controller tests
Run:
node --test tests/test-calendar-view.js
Expected: 9 tests pass.
- Step 5: Run a JavaScript syntax check
Run:
node --check assets/js/aa-events-calendar-view.js
Expected: exit status 0 with no output.
- Step 6: Commit the controller slice when Git is available
git add assets/js/aa-events-calendar-view.js tests/test-calendar-view.js
git commit -m "feat: persist calendar view preference"
In the current non-Git workspace, record the commit as skipped.
Task 4: Enqueue the Controller and Restore Version Consistency
Files:
-
Modify:
tests/bootstrap.php -
Create:
tests/test-frontend-assets.php -
Modify:
tests/run.php -
Modify:
includes/class-aa-events.php -
Modify:
aa-events.php -
Modify:
tests/test-admin-consumers.php -
Step 1: Add enqueue test doubles
In tests/bootstrap.php, change the test version and add capture globals near the top:
define( 'AA_EVENTS_VERSION', '1.2.1' );
$GLOBALS['aa_events_test_styles'] = array();
$GLOBALS['aa_events_test_scripts'] = array();
Add these functions before the assertion helpers:
function get_stylesheet_directory() {
return __DIR__ . '/fixtures/theme-without-aa-events-override';
}
function get_stylesheet_directory_uri() {
return 'https://example.test/theme';
}
function wp_enqueue_style( $handle, $src, $dependencies = array(), $version = false ) {
$GLOBALS['aa_events_test_styles'][ $handle ] = array( $src, $dependencies, $version );
}
function wp_enqueue_script( $handle, $src, $dependencies = array(), $version = false, $in_footer = false ) {
$GLOBALS['aa_events_test_scripts'][ $handle ] = array( $src, $dependencies, $version, $in_footer );
}
- Step 2: Create the failing frontend enqueue test
Create tests/test-frontend-assets.php:
<?php
if ( ! defined( 'WPINC' ) ) {
define( 'WPINC', 'tests' );
}
if ( ! defined( 'AA_EVENTS_PLUGIN_DIR' ) ) {
define( 'AA_EVENTS_PLUGIN_DIR', dirname( __DIR__ ) . '/' );
}
if ( ! defined( 'AA_EVENTS_PLUGIN_URL' ) ) {
define( 'AA_EVENTS_PLUGIN_URL', 'https://example.test/plugins/aa-events/' );
}
require_once __DIR__ . '/../includes/class-aa-events.php';
$GLOBALS['aa_events_test_styles'] = array();
$GLOBALS['aa_events_test_scripts'] = array();
AA_Events::instance()->enqueue_styles();
aa_events_assert_same(
array(
'https://example.test/plugins/aa-events/assets/js/aa-events-calendar-view.js',
array(),
AA_EVENTS_VERSION,
true,
),
$GLOBALS['aa_events_test_scripts']['aa-events-calendar-view'] ?? null,
'Frontend assets enqueue the dependency-free calendar view controller in the footer.'
);
Add this line to tests/run.php after test-calendar-css.php:
require_once __DIR__ . '/test-frontend-assets.php';
- Step 3: Run the PHP suite and verify the enqueue assertion fails
Run:
php tests/run.php
Expected: FAIL because aa-events-calendar-view has not been enqueued. The two known version-consistency assertions also remain failing until Step 5.
- Step 4: Enqueue the browser controller
At the end of AA_Events::enqueue_styles() in includes/class-aa-events.php, add:
wp_enqueue_script(
'aa-events-calendar-view',
AA_EVENTS_PLUGIN_URL . 'assets/js/aa-events-calendar-view.js',
array(),
AA_EVENTS_VERSION,
true
);
Update the method docblock summary from Enqueue styles. to:
/**
* Enqueue frontend assets.
*/
- Step 5: Align the existing release version
In aa-events.php, change:
define( 'AA_EVENTS_VERSION', '1.2.1' );
In tests/test-admin-consumers.php, change the hard-coded release assertion to:
aa_events_assert_same( '1.2.1', $header_match[1] ?? '', 'Plugin header uses the feature release version.' );
This resolves the baseline mismatch where the existing plugin header is already 1.2.1 but the asset constant and test fixtures are still 1.2.0.
- Step 6: Run the PHP suite
Run:
php tests/run.php
Expected: All AA Events tests passed.
- Step 7: Commit the integration slice when Git is available
git add tests/bootstrap.php tests/test-frontend-assets.php tests/run.php includes/class-aa-events.php aa-events.php tests/test-admin-consumers.php
git commit -m "feat: load calendar view controller"
In the current non-Git workspace, record the commit as skipped.
Task 5: Document and Verify the Feature
Files:
-
Modify:
README.md -
Step 1: Update frontend-view documentation
Replace the desktop/mobile calendar bullets in README.md with:
- On desktop and tablet, visitors can switch between Calendar and List. Calendar is the initial default; the selected view is saved in that browser and restored after month navigation or a later visit.
- Calendar view uses a traditional month grid. Multi-day occurrences render as bars spanning each week they cross, with the event title repeated on each weekly segment.
- At `640px` wide and below, the switcher is hidden and the calendar always uses the chronological List view, regardless of the saved desktop preference.
Replace the final theme-override paragraph with:
An existing `your-theme/aa-events/calendar.php` remains authoritative. It does not automatically acquire the plugin template’s week-spanning bars, List markup, or desktop view switcher. Theme authors can update overrides using `aa_events_get_occurrences()`, `aa_events_format_occurrence()`, and `aa_events_occurrence_time_markup()`. A CSS override must also account for any calendar, agenda, and switcher markup it adopts.
- Step 2: Run all automated verification
Run:
php tests/run.php
node --test tests/test-calendar-view.js
php -l templates/calendar.php
php -l includes/class-aa-events.php
node --check assets/js/aa-events-calendar-view.js
cmp -s assets/css/aa-events.css templates/aa-events.css
Expected:
-
PHP suite prints
All AA Events tests passed. -
Node reports 9 passing tests and 0 failures.
-
Both PHP syntax checks report no syntax errors.
-
JavaScript syntax check exits 0.
-
CSS comparison exits 0.
-
Step 3: Perform focused source checks
Run:
rg -n "data-aa-events-(calendar|view-switcher|view-button|view-panel)" templates/calendar.php
rg -n "aa-events-calendar-view|localStorage|matchMedia" assets/js/aa-events-calendar-view.js includes/class-aa-events.php
rg -n "switch|saved|640px|override" README.md
Expected: template hooks, persistence/breakpoint code, enqueue path, and documentation are all present.
- Step 4: Manually verify in WordPress
On a page using the Events Calendar template:
- At a width above 640 pixels, confirm Calendar is initially selected.
- Use the keyboard to select List and confirm focus remains visible.
- Navigate to the next month and reload; confirm List remains selected.
- Return to Calendar and confirm that preference persists.
- Resize to exactly 640 pixels; confirm the switcher and grid are hidden and List is visible.
- Resize above 640 pixels; confirm the saved desktop choice returns.
- Render two calendars on one page if the host theme permits it; selecting either switcher must update both.
- Disable site storage or make it unavailable; confirm switching still works for the current page without console errors.
- Step 5: Commit documentation when Git is available
git add README.md
git commit -m "docs: explain persistent calendar views"
In the current non-Git workspace, record the commit as skipped.