Remove docs folder
This commit is contained in:
@@ -1,133 +0,0 @@
|
||||
# Event Contact Email 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 optional event contact email field and display valid values as protected email links on single-event pages.
|
||||
|
||||
**Architecture:** Register `event_contact_email` in the built-in ACF group and read the same field name from built-in or admin-created groups. Keep output in the single-event template, sanitizing before conditional rendering and applying WordPress escaping at the attribute and visible-HTML boundaries.
|
||||
|
||||
**Tech Stack:** PHP 7.4+, WordPress template APIs, ACF PRO, existing dependency-free PHP tests
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Register the Contact Email Field
|
||||
|
||||
**Files:**
|
||||
- Modify: `includes/acf-fields.php`
|
||||
- Modify: `tests/bootstrap.php`
|
||||
- Modify: `tests/test-acf-fields.php`
|
||||
|
||||
- [ ] **Step 1: Write the failing registration test**
|
||||
|
||||
Extend the ACF test double to capture `acf_add_local_field_group()` arguments. In `tests/test-acf-fields.php`, clear admin groups, call `events_register_acf_fields()`, locate the field named `event_contact_email`, and assert this exact contract:
|
||||
|
||||
```php
|
||||
array(
|
||||
'key' => 'field_aa_events_contact_email',
|
||||
'label' => 'Contact Email',
|
||||
'name' => 'event_contact_email',
|
||||
'type' => 'email',
|
||||
'required' => 0,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the suite and verify RED**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: FAIL because no field named `event_contact_email` is registered.
|
||||
|
||||
- [ ] **Step 3: Add the built-in field**
|
||||
|
||||
Add the optional ACF email field after location/URL/address details in `includes/acf-fields.php`, using the stable key and contract above plus standard empty wrapper/default/placeholder settings.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
php -l includes/acf-fields.php
|
||||
```
|
||||
|
||||
Expected: all tests pass and syntax is valid.
|
||||
|
||||
### Task 2: Render Contact Email on Single Events
|
||||
|
||||
**Files:**
|
||||
- Modify: `templates/single-event.php`
|
||||
- Modify: `tests/bootstrap.php`
|
||||
- Create: `tests/test-contact-email.php`
|
||||
- Modify: `tests/run.php`
|
||||
|
||||
- [ ] **Step 1: Write failing output tests**
|
||||
|
||||
Add minimal `sanitize_email()` and `antispambot()` doubles if absent. Test a focused helper named `aa_events_contact_email_markup()` with:
|
||||
|
||||
```php
|
||||
aa_events_assert_same( '', aa_events_contact_email_markup( '' ), 'Empty email renders nothing.' );
|
||||
aa_events_assert_same( '', aa_events_contact_email_markup( 'not an email' ), 'Invalid email renders nothing.' );
|
||||
$markup = aa_events_contact_email_markup( 'events@example.com' );
|
||||
aa_events_assert_true( false !== strpos( $markup, 'mailto:' ), 'Valid email has a mailto link.' );
|
||||
aa_events_assert_true( false !== strpos( $markup, 'events@example.com' ), 'Valid email remains readable.' );
|
||||
```
|
||||
|
||||
Also assert `templates/single-event.php` reads `event_contact_email`, calls the helper, and renders the `Contact Email:` label only when markup is non-empty.
|
||||
|
||||
- [ ] **Step 2: Run the suite and verify RED**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: FAIL because `aa_events_contact_email_markup()` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement safe markup and template output**
|
||||
|
||||
Add this focused template helper to `includes/template-loader.php`:
|
||||
|
||||
```php
|
||||
function aa_events_contact_email_markup( $value ) {
|
||||
$email = sanitize_email( (string) $value );
|
||||
if ( '' === $email || $email !== trim( (string) $value ) ) {
|
||||
return '';
|
||||
}
|
||||
$protected = antispambot( $email );
|
||||
return '<a href="' . esc_attr( 'mailto:' . $protected ) . '">' . wp_kses_post( $protected ) . '</a>';
|
||||
}
|
||||
```
|
||||
|
||||
In `templates/single-event.php`, call the helper with `get_field( 'event_contact_email' )`. When non-empty, render an `.aa-event-contact-email` detail row with localized label `Contact Email:` and output only the helper's already constrained anchor through `wp_kses_post()`.
|
||||
|
||||
- [ ] **Step 4: Run complete verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
php -l includes/template-loader.php
|
||||
php -l templates/single-event.php
|
||||
phpcs --standard=WordPress includes/acf-fields.php includes/template-loader.php templates/single-event.php
|
||||
```
|
||||
|
||||
Expected: tests and syntax pass. Record unrelated pre-existing PHPCS findings separately rather than changing unrelated template code.
|
||||
|
||||
### Task 3: Documentation and Final Check
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Document the optional field**
|
||||
|
||||
Add Contact Email to the Event Details list and state that it appears only on single-event pages when valid.
|
||||
|
||||
- [ ] **Step 2: Run final verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
find . -path './.superpowers' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||
```
|
||||
|
||||
Expected: all tests and PHP syntax checks pass.
|
||||
|
||||
The workspace is not a Git repository, so commit steps are unavailable.
|
||||
@@ -1,879 +0,0 @@
|
||||
# Multi-Day Event Occurrences 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:** Support mixed recurring and multi-day event occurrences, render multi-day bars across the traditional month grid, and provide a chronological mobile agenda.
|
||||
|
||||
**Architecture:** Keep `event_dates` as the persisted ACF repeater and normalize all supported field formats through a focused occurrence API. Put date normalization/formatting in `includes/occurrences.php` and pure calendar segmentation/lane logic in `includes/calendar-layout.php`; templates consume those APIs without interpreting raw ACF rows. Desktop rendering uses week containers with a seven-column CSS grid, while mobile renders the same normalized occurrences as an agenda.
|
||||
|
||||
**Tech Stack:** PHP 7.4+, WordPress APIs, Advanced Custom Fields, semantic HTML, CSS Grid, dependency-free PHP test runner
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create `includes/occurrences.php`: normalize ACF/legacy values, validate ranges, format occurrence labels, and migrate legacy values on save.
|
||||
- Create `includes/calendar-layout.php`: calculate month bounds, split occurrences into weekly segments, and assign overlap lanes.
|
||||
- Create `tests/bootstrap.php`: minimal WordPress/ACF stubs and assertion helpers for dependency-free unit tests.
|
||||
- Create `tests/test-occurrences.php`: normalization, formatting, validation, and migration tests.
|
||||
- Create `tests/test-calendar-layout.php`: month intersection, week splitting, and lane-allocation tests.
|
||||
- Create `tests/run.php`: execute all test files and return a nonzero status on failure.
|
||||
- Modify `includes/class-aa-events.php`: load the two focused helper modules before post type/template code.
|
||||
- Modify `includes/acf-fields.php`: extend and relabel the repeater, register validation, and make the legacy field less prominent.
|
||||
- Modify `assets/js/aa-events-admin.js`: clear/disable time inputs while an occurrence is marked all-day.
|
||||
- Modify `includes/post-types.php`: replace raw/mismatched repeater reads with normalized occurrences and update admin date sorting.
|
||||
- Modify `templates/single-event.php`: render formatted occurrence ranges.
|
||||
- Modify `templates/archive-event.php`: render formatted occurrence ranges.
|
||||
- Rewrite `templates/calendar.php`: collect intersecting occurrences and render desktop week grids plus mobile agenda markup.
|
||||
- Modify `assets/css/aa-events.css`: style the week grid, spanning bars, continuations, focus states, and mobile agenda.
|
||||
- Modify `templates/aa-events.css`: keep the documented theme-copy stylesheet synchronized with the bundled stylesheet.
|
||||
- Modify `README.md`: document occurrence fields, compatibility, responsive behavior, and theme-override implications.
|
||||
|
||||
The current directory is not a Git worktree. Commit steps below are required once this plugin is placed in a repository; until then, record each checkpoint with test output and skip only the `git add`/`git commit` commands.
|
||||
|
||||
### Task 1: Add a Dependency-Free PHP Test Harness
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/bootstrap.php`
|
||||
- Create: `tests/run.php`
|
||||
|
||||
- [ ] **Step 1: Create WordPress and ACF test doubles**
|
||||
|
||||
Create `tests/bootstrap.php` with mutable field storage and strict assertion helpers:
|
||||
|
||||
```php
|
||||
<?php
|
||||
define( 'ABSPATH', __DIR__ . '/' );
|
||||
|
||||
$GLOBALS['aa_events_test_fields'] = array();
|
||||
$GLOBALS['aa_events_test_updates'] = array();
|
||||
$GLOBALS['aa_events_test_failures'] = 0;
|
||||
|
||||
function get_field( $name, $post_id = false ) {
|
||||
return $GLOBALS['aa_events_test_fields'][ (int) $post_id ][ $name ] ?? null;
|
||||
}
|
||||
|
||||
function update_field( $name, $value, $post_id = false ) {
|
||||
$GLOBALS['aa_events_test_fields'][ (int) $post_id ][ $name ] = $value;
|
||||
$GLOBALS['aa_events_test_updates'][] = array( $name, $value, (int) $post_id );
|
||||
return true;
|
||||
}
|
||||
|
||||
function wp_timezone() {
|
||||
return new DateTimeZone( 'America/Winnipeg' );
|
||||
}
|
||||
|
||||
function wp_date( $format, $timestamp = null, $timezone = null ) {
|
||||
$date = new DateTimeImmutable( '@' . ( null === $timestamp ? time() : $timestamp ) );
|
||||
return $date->setTimezone( $timezone ?: wp_timezone() )->format( $format );
|
||||
}
|
||||
|
||||
function __( $text ) { return $text; }
|
||||
function _x( $text ) { return $text; }
|
||||
|
||||
function aa_events_test_reset() {
|
||||
$GLOBALS['aa_events_test_fields'] = array();
|
||||
$GLOBALS['aa_events_test_updates'] = array();
|
||||
}
|
||||
|
||||
function aa_events_assert_same( $expected, $actual, $message ) {
|
||||
if ( $expected !== $actual ) {
|
||||
++$GLOBALS['aa_events_test_failures'];
|
||||
fwrite( STDERR, "FAIL: {$message}\nExpected: " . var_export( $expected, true ) . "\nActual: " . var_export( $actual, true ) . "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
function aa_events_assert_true( $actual, $message ) {
|
||||
aa_events_assert_same( true, (bool) $actual, $message );
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the test entry point**
|
||||
|
||||
Create `tests/run.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
require dirname( __DIR__ ) . '/includes/occurrences.php';
|
||||
require dirname( __DIR__ ) . '/includes/calendar-layout.php';
|
||||
require __DIR__ . '/test-occurrences.php';
|
||||
require __DIR__ . '/test-calendar-layout.php';
|
||||
|
||||
if ( $GLOBALS['aa_events_test_failures'] > 0 ) {
|
||||
fwrite( STDERR, $GLOBALS['aa_events_test_failures'] . " test(s) failed.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
fwrite( STDOUT, "All AA Events tests passed.\n" );
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the empty harness**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: FAIL because `includes/occurrences.php` does not exist. This confirms the test command detects missing production code.
|
||||
|
||||
- [ ] **Step 4: Commit the harness when Git is available**
|
||||
|
||||
```bash
|
||||
git add tests/bootstrap.php tests/run.php
|
||||
git commit -m "test: add lightweight event helper harness"
|
||||
```
|
||||
|
||||
### Task 2: Normalize and Format Occurrences
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test-occurrences.php`
|
||||
- Create: `includes/occurrences.php`
|
||||
- Modify: `includes/class-aa-events.php`
|
||||
|
||||
- [ ] **Step 1: Write failing normalization tests**
|
||||
|
||||
Create `tests/test-occurrences.php` with these cases:
|
||||
|
||||
```php
|
||||
<?php
|
||||
aa_events_test_reset();
|
||||
$GLOBALS['aa_events_test_fields'][10]['event_dates'] = array(
|
||||
array(
|
||||
'date' => '2026-07-10',
|
||||
'start_time' => '18:00:00',
|
||||
'end_date' => '2026-07-12',
|
||||
'end_time' => '14:00:00',
|
||||
'all_day' => false,
|
||||
),
|
||||
array(
|
||||
'date' => '2026-07-20',
|
||||
'start_time' => '',
|
||||
'end_date' => '',
|
||||
'end_time' => '',
|
||||
'all_day' => true,
|
||||
),
|
||||
);
|
||||
$occurrences = aa_events_get_occurrences( 10 );
|
||||
aa_events_assert_same( 2, count( $occurrences ), 'Repeater rows normalize.' );
|
||||
aa_events_assert_same( '2026-07-12', $occurrences[0]['end_date'], 'Explicit end date is retained.' );
|
||||
aa_events_assert_same( '2026-07-20', $occurrences[1]['end_date'], 'Missing end date defaults to start date.' );
|
||||
aa_events_assert_true( $occurrences[1]['all_day'], 'All-day flag normalizes to boolean.' );
|
||||
|
||||
aa_events_test_reset();
|
||||
$GLOBALS['aa_events_test_fields'][11]['event_datetime'] = '2026-08-03 09:30:00';
|
||||
$legacy = aa_events_get_occurrences( 11 );
|
||||
aa_events_assert_same( '2026-08-03', $legacy[0]['start_date'], 'Legacy datetime supplies start date.' );
|
||||
aa_events_assert_same( '09:30:00', $legacy[0]['start_time'], 'Legacy datetime supplies start time.' );
|
||||
aa_events_assert_same( 'event_datetime', $legacy[0]['source'], 'Legacy source is identified.' );
|
||||
|
||||
aa_events_test_reset();
|
||||
$GLOBALS['aa_events_test_fields'][12]['date'] = '2026-09-01';
|
||||
$GLOBALS['aa_events_test_fields'][12]['start_time'] = '11:00:00';
|
||||
$separate = aa_events_get_occurrences( 12 );
|
||||
aa_events_assert_same( 'legacy_separate', $separate[0]['source'], 'Separate legacy fields normalize.' );
|
||||
|
||||
aa_events_test_reset();
|
||||
$GLOBALS['aa_events_test_fields'][13]['event_dates'] = array(
|
||||
array( 'date' => '', 'end_date' => '2026-07-02' ),
|
||||
array( 'date' => 'not-a-date' ),
|
||||
);
|
||||
aa_events_assert_same( array(), aa_events_get_occurrences( 13 ), 'Malformed rows are skipped.' );
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add failing validation and formatting tests**
|
||||
|
||||
Append to `tests/test-occurrences.php`:
|
||||
|
||||
```php
|
||||
aa_events_assert_same(
|
||||
'End date cannot be earlier than start date.',
|
||||
aa_events_validate_occurrence_row(
|
||||
array( 'date' => '2026-07-12', 'end_date' => '2026-07-10' )
|
||||
),
|
||||
'Backwards date range is rejected.'
|
||||
);
|
||||
aa_events_assert_same(
|
||||
'End time cannot be earlier than start time on the same date.',
|
||||
aa_events_validate_occurrence_row(
|
||||
array(
|
||||
'date' => '2026-07-10',
|
||||
'start_time' => '18:00:00',
|
||||
'end_time' => '14:00:00',
|
||||
)
|
||||
),
|
||||
'Backwards same-day time range is rejected.'
|
||||
);
|
||||
aa_events_assert_same(
|
||||
'',
|
||||
aa_events_validate_occurrence_row(
|
||||
array( 'date' => '2026-07-10', 'end_date' => '2026-07-12', 'all_day' => true )
|
||||
),
|
||||
'Valid all-day range passes.'
|
||||
);
|
||||
|
||||
$timed = aa_events_normalize_occurrence_row(
|
||||
array(
|
||||
'date' => '2026-07-10',
|
||||
'start_time' => '18:00:00',
|
||||
'end_date' => '2026-07-12',
|
||||
'end_time' => '14:00:00',
|
||||
),
|
||||
'event_dates'
|
||||
);
|
||||
aa_events_assert_same(
|
||||
'July 10, 2026, 6:00 PM–July 12, 2026, 2:00 PM',
|
||||
aa_events_format_occurrence( $timed ),
|
||||
'Timed multi-day range formats both endpoints.'
|
||||
);
|
||||
|
||||
$all_day = aa_events_normalize_occurrence_row(
|
||||
array( 'date' => '2026-07-10', 'end_date' => '2026-07-12', 'all_day' => true ),
|
||||
'event_dates'
|
||||
);
|
||||
aa_events_assert_same(
|
||||
'July 10–12, 2026 — All day',
|
||||
aa_events_format_occurrence( $all_day ),
|
||||
'All-day range uses compact localized dates.'
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests to verify they fail**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: FAIL with undefined occurrence function errors.
|
||||
|
||||
- [ ] **Step 4: Implement the occurrence API**
|
||||
|
||||
Create `includes/occurrences.php`. Implement these exact public functions:
|
||||
|
||||
```php
|
||||
function aa_events_normalize_occurrence_row( array $row, $source = 'event_dates', $post_id = 0 )
|
||||
function aa_events_get_occurrences( $post_id )
|
||||
function aa_events_validate_occurrence_row( array $row )
|
||||
function aa_events_format_occurrence( array $occurrence )
|
||||
function aa_events_occurrence_start( array $occurrence )
|
||||
function aa_events_occurrence_end( array $occurrence )
|
||||
function aa_events_migrate_legacy_occurrence( $post_id )
|
||||
```
|
||||
|
||||
`aa_events_normalize_occurrence_row()` must return `null` for invalid start dates and otherwise return this stable shape:
|
||||
|
||||
```php
|
||||
array(
|
||||
'post_id' => (int) $post_id,
|
||||
'start_date' => 'Y-m-d',
|
||||
'start_time' => 'H:i:s or empty string',
|
||||
'end_date' => 'Y-m-d',
|
||||
'end_time' => 'H:i:s or empty string',
|
||||
'all_day' => (bool) $all_day,
|
||||
'source' => 'event_dates|event_datetime|legacy_separate',
|
||||
)
|
||||
```
|
||||
|
||||
Use strict `DateTimeImmutable::createFromFormat()` parsing in `wp_timezone()`, check `DateTimeImmutable::getLastErrors()` when it returns an array, ignore times when `all_day` is true, and sort normalized occurrences by `aa_events_occurrence_start()`.
|
||||
|
||||
`aa_events_format_occurrence()` must use `wp_date()` for every visible date/time and cover the five formats specified in the design. Use an en dash between endpoints and an em dash before “All day.”
|
||||
|
||||
- [ ] **Step 5: Load occurrence helpers before consumers**
|
||||
|
||||
In `AA_Events::includes()`, add this as the first include:
|
||||
|
||||
```php
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/occurrences.php';
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run occurrence tests**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: occurrence assertions pass; execution then fails because calendar-layout files do not yet exist.
|
||||
|
||||
- [ ] **Step 7: Commit the normalized occurrence API when Git is available**
|
||||
|
||||
```bash
|
||||
git add includes/occurrences.php includes/class-aa-events.php tests/test-occurrences.php
|
||||
git commit -m "feat: normalize event occurrences"
|
||||
```
|
||||
|
||||
### Task 3: Extend the ACF Editor, Validate Rows, and Migrate Legacy Data
|
||||
|
||||
**Files:**
|
||||
- Modify: `includes/acf-fields.php`
|
||||
- Modify: `includes/occurrences.php`
|
||||
- Modify: `assets/js/aa-events-admin.js`
|
||||
- Modify: `tests/test-occurrences.php`
|
||||
|
||||
- [ ] **Step 1: Write failing migration tests**
|
||||
|
||||
Append to `tests/test-occurrences.php`:
|
||||
|
||||
```php
|
||||
aa_events_test_reset();
|
||||
$GLOBALS['aa_events_test_fields'][20]['event_datetime'] = '2026-10-05 13:15:00';
|
||||
aa_events_assert_true( aa_events_migrate_legacy_occurrence( 20 ), 'Legacy value migrates.' );
|
||||
aa_events_assert_same(
|
||||
array(
|
||||
array(
|
||||
'date' => '2026-10-05',
|
||||
'start_time' => '13:15:00',
|
||||
'end_date' => '',
|
||||
'end_time' => '',
|
||||
'all_day' => 0,
|
||||
),
|
||||
),
|
||||
$GLOBALS['aa_events_test_fields'][20]['event_dates'],
|
||||
'Migration writes one compatible repeater row.'
|
||||
);
|
||||
aa_events_assert_same( false, aa_events_migrate_legacy_occurrence( 20 ), 'Second migration is idempotent.' );
|
||||
aa_events_assert_same( 1, count( $GLOBALS['aa_events_test_updates'] ), 'Second migration writes nothing.' );
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run migration test to verify it fails**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: FAIL because migration is not yet implemented or does not write the required row shape.
|
||||
|
||||
- [ ] **Step 3: Implement idempotent migration and save hook**
|
||||
|
||||
Complete `aa_events_migrate_legacy_occurrence()` so it returns `false` when `event_dates` already has rows or no parseable legacy value exists. Otherwise, call `update_field( 'event_dates', array( $row ), $post_id )` and return its boolean result.
|
||||
|
||||
Register a save callback that ignores autosaves/revisions and non-event posts:
|
||||
|
||||
```php
|
||||
function aa_events_migrate_legacy_on_save( $post_id ) {
|
||||
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
if ( 'event' !== get_post_type( $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
aa_events_migrate_legacy_occurrence( $post_id );
|
||||
}
|
||||
add_action( 'acf/save_post', 'aa_events_migrate_legacy_on_save', 20 );
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace the built-in repeater definition**
|
||||
|
||||
In `includes/acf-fields.php`, retain field key `field_61b0c7f0a3e8f` and name `event_dates`, but change its label/button/instructions to:
|
||||
|
||||
```php
|
||||
'label' => 'Occurrences',
|
||||
'instructions' => 'Each row is one occurrence. Add an end date for a continuous multi-day event; add more rows when the event recurs.',
|
||||
'layout' => 'row',
|
||||
'button_label' => 'Add Occurrence',
|
||||
```
|
||||
|
||||
Keep `date` and `start_time`, then add subfields with unique stable keys:
|
||||
|
||||
```php
|
||||
array(
|
||||
'key' => 'field_aa_events_end_date',
|
||||
'label' => 'End Date',
|
||||
'name' => 'end_date',
|
||||
'type' => 'date_picker',
|
||||
'required' => 0,
|
||||
'display_format'=> 'F j, Y',
|
||||
'return_format' => 'Y-m-d',
|
||||
'first_day' => 1,
|
||||
),
|
||||
array(
|
||||
'key' => 'field_aa_events_end_time',
|
||||
'label' => 'End Time',
|
||||
'name' => 'end_time',
|
||||
'type' => 'time_picker',
|
||||
'required' => 0,
|
||||
'display_format'=> 'g:i a',
|
||||
'return_format' => 'H:i:s',
|
||||
),
|
||||
array(
|
||||
'key' => 'field_aa_events_all_day',
|
||||
'label' => 'All Day',
|
||||
'name' => 'all_day',
|
||||
'type' => 'true_false',
|
||||
'ui' => 1,
|
||||
'default_value' => 0,
|
||||
),
|
||||
```
|
||||
|
||||
Change the legacy `event_datetime` instructions to “Legacy compatibility field. Use Occurrences for new events.”
|
||||
|
||||
- [ ] **Step 5: Register row validation through ACF**
|
||||
|
||||
Add `acf/validate_value/key=field_61b0c7f0a3e8f` validation that loops submitted rows, maps ACF subfield keys to semantic names, calls `aa_events_validate_occurrence_row()`, and returns a translated row-numbered error such as `Occurrence 2: End date cannot be earlier than start date.` Return the incoming `$valid` unchanged when it is already an error.
|
||||
|
||||
- [ ] **Step 6: Implement all-day editor behavior**
|
||||
|
||||
In `assets/js/aa-events-admin.js`, add a delegated `change` handler scoped to `.acf-field[data-name="event_dates"]`. For each repeater row, toggle an `aa-events-is-all-day` class, disable the `start_time` and `end_time` inputs while checked, and clear those inputs when the checkbox changes from unchecked to checked. Run this synchronization on ACF `ready` and `append` actions.
|
||||
|
||||
- [ ] **Step 7: Run tests and PHP syntax checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
php -l includes/acf-fields.php
|
||||
php -l includes/occurrences.php
|
||||
```
|
||||
|
||||
Expected: all occurrence tests pass and both files report `No syntax errors detected`.
|
||||
|
||||
- [ ] **Step 8: Commit editor and migration behavior when Git is available**
|
||||
|
||||
```bash
|
||||
git add includes/acf-fields.php includes/occurrences.php assets/js/aa-events-admin.js tests/test-occurrences.php
|
||||
git commit -m "feat: add occurrence ranges to event editor"
|
||||
```
|
||||
|
||||
### Task 4: Build and Test Calendar Segmentation
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test-calendar-layout.php`
|
||||
- Create: `includes/calendar-layout.php`
|
||||
- Modify: `includes/class-aa-events.php`
|
||||
|
||||
- [ ] **Step 1: Write failing week-segment tests**
|
||||
|
||||
Create `tests/test-calendar-layout.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$month = aa_events_calendar_month( 2026, 7, 1 );
|
||||
aa_events_assert_same( '2026-06-29', $month['grid_start']->format( 'Y-m-d' ), 'Monday-start grid begins before month.' );
|
||||
aa_events_assert_same( '2026-08-02', $month['grid_end']->format( 'Y-m-d' ), 'Grid ends after complete final week.' );
|
||||
|
||||
$occurrence = aa_events_normalize_occurrence_row(
|
||||
array(
|
||||
'date' => '2026-07-10',
|
||||
'start_time' => '18:00:00',
|
||||
'end_date' => '2026-07-15',
|
||||
'end_time' => '14:00:00',
|
||||
),
|
||||
'event_dates',
|
||||
30
|
||||
);
|
||||
$segments = aa_events_calendar_segments( $occurrence, $month );
|
||||
aa_events_assert_same( 2, count( $segments ), 'Occurrence splits at week boundary.' );
|
||||
aa_events_assert_same( 4, $segments[0]['start_column'], 'Friday is column four in Monday-start grid.' );
|
||||
aa_events_assert_same( 3, $segments[0]['span'], 'First segment covers Friday through Sunday.' );
|
||||
aa_events_assert_true( $segments[0]['continues_after'], 'First segment continues next week.' );
|
||||
aa_events_assert_same( 0, $segments[1]['start_column'], 'Second segment starts Monday.' );
|
||||
aa_events_assert_same( 3, $segments[1]['span'], 'Second segment covers Monday through Wednesday.' );
|
||||
aa_events_assert_true( $segments[1]['continues_before'], 'Second segment continued from prior week.' );
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add failing clipping and lane tests**
|
||||
|
||||
Append:
|
||||
|
||||
```php
|
||||
$cross_month = aa_events_normalize_occurrence_row(
|
||||
array( 'date' => '2026-06-25', 'end_date' => '2026-07-03', 'all_day' => true ),
|
||||
'event_dates',
|
||||
31
|
||||
);
|
||||
$clipped = aa_events_calendar_segments( $cross_month, $month );
|
||||
aa_events_assert_true( $clipped[0]['continues_before'], 'Month-clipped segment shows prior continuation.' );
|
||||
|
||||
$week_segments = array(
|
||||
array( 'start_column' => 0, 'span' => 3, 'post_id' => 1 ),
|
||||
array( 'start_column' => 2, 'span' => 2, 'post_id' => 2 ),
|
||||
array( 'start_column' => 4, 'span' => 2, 'post_id' => 3 ),
|
||||
);
|
||||
$lanes = aa_events_assign_segment_lanes( $week_segments );
|
||||
aa_events_assert_same( 0, $lanes[0]['lane'], 'First segment uses first lane.' );
|
||||
aa_events_assert_same( 1, $lanes[1]['lane'], 'Overlapping segment uses second lane.' );
|
||||
aa_events_assert_same( 0, $lanes[2]['lane'], 'Non-overlapping segment reuses first lane.' );
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests to verify failure**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: FAIL with undefined calendar-layout functions.
|
||||
|
||||
- [ ] **Step 4: Implement pure layout functions**
|
||||
|
||||
Create `includes/calendar-layout.php` with these public functions:
|
||||
|
||||
```php
|
||||
function aa_events_calendar_month( $year, $month, $start_of_week )
|
||||
function aa_events_occurrence_intersects_range( array $occurrence, DateTimeImmutable $start, DateTimeImmutable $end )
|
||||
function aa_events_calendar_segments( array $occurrence, array $month )
|
||||
function aa_events_assign_segment_lanes( array $segments )
|
||||
function aa_events_group_calendar_weeks( array $occurrences, array $month )
|
||||
```
|
||||
|
||||
`aa_events_calendar_month()` must return `month_start`, `month_end`, `grid_start`, `grid_end`, `start_of_week`, and an ordered `weeks` array. Each week contains `start`, `end`, and seven `days`.
|
||||
|
||||
`aa_events_calendar_segments()` must clip to `month_start`/`month_end`, split at each configured week boundary, use zero-based columns, and set `continues_before`/`continues_after` relative to the true occurrence interval.
|
||||
|
||||
`aa_events_assign_segment_lanes()` must sort by start column, then longest span, then post ID. Assign the first lane whose occupied end column is strictly before the segment start column.
|
||||
|
||||
`aa_events_group_calendar_weeks()` must return each week with lane-assigned segments and `lane_count`, preserving complete occurrence data on every segment for template labels.
|
||||
|
||||
- [ ] **Step 5: Load the layout helper**
|
||||
|
||||
In `AA_Events::includes()`, immediately after the occurrence include, add:
|
||||
|
||||
```php
|
||||
include_once AA_EVENTS_PLUGIN_DIR . 'includes/calendar-layout.php';
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run layout tests**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: `All AA Events tests passed.`
|
||||
|
||||
- [ ] **Step 7: Commit layout logic when Git is available**
|
||||
|
||||
```bash
|
||||
git add includes/calendar-layout.php includes/class-aa-events.php tests/test-calendar-layout.php
|
||||
git commit -m "feat: calculate multi-day calendar segments"
|
||||
```
|
||||
|
||||
### Task 5: Move All Existing Consumers to the Occurrence API
|
||||
|
||||
**Files:**
|
||||
- Modify: `includes/post-types.php`
|
||||
- Modify: `templates/single-event.php`
|
||||
- Modify: `templates/archive-event.php`
|
||||
|
||||
- [ ] **Step 1: Replace the compatibility helper**
|
||||
|
||||
Keep `events_get_all_event_dates()` as a deprecated compatibility wrapper so theme overrides do not fatally break:
|
||||
|
||||
```php
|
||||
function events_get_all_event_dates( $post_id ) {
|
||||
$dates = array();
|
||||
foreach ( aa_events_get_occurrences( $post_id ) as $occurrence ) {
|
||||
$dates[] = array(
|
||||
'date' => $occurrence['start_date'],
|
||||
'time' => $occurrence['start_time'],
|
||||
'end_date' => $occurrence['end_date'],
|
||||
'end_time' => $occurrence['end_time'],
|
||||
'all_day' => $occurrence['all_day'],
|
||||
);
|
||||
}
|
||||
return $dates;
|
||||
}
|
||||
```
|
||||
|
||||
Remove direct reads of the nonexistent/mismatched `multiple_dates` field from admin display code.
|
||||
|
||||
- [ ] **Step 2: Update the admin column display**
|
||||
|
||||
Call `aa_events_get_occurrences( $post_id )`, display `aa_events_format_occurrence( $occurrences[0] )`, and retain the existing `+N` badge with escaped count. If no normalized occurrence exists, render an em dash.
|
||||
|
||||
- [ ] **Step 3: Correct admin SQL sorting keys**
|
||||
|
||||
Change the first repeater date join from `multiple_dates_0_date` to `event_dates_0_date`. Keep the `COALESCE` fallback order `event_dates_0_date`, `event_datetime`, `date`, post date. Add `$wpdb->posts.ID` as a final deterministic ordering key.
|
||||
|
||||
- [ ] **Step 4: Update archive and single templates**
|
||||
|
||||
In both templates, replace local timestamp construction with:
|
||||
|
||||
```php
|
||||
$occurrences = aa_events_get_occurrences( get_the_ID() );
|
||||
foreach ( $occurrences as $index => $occurrence ) {
|
||||
$start = aa_events_occurrence_start( $occurrence );
|
||||
printf(
|
||||
'<time datetime="%1$s">%2$s</time>%3$s',
|
||||
esc_attr( $start->format( DATE_W3C ) ),
|
||||
esc_html( aa_events_format_occurrence( $occurrence ) ),
|
||||
$index < count( $occurrences ) - 1 ? '<br />' : ''
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Change the visible label from “Date:” to the plural-aware `Dates:` only when more than one occurrence exists.
|
||||
|
||||
- [ ] **Step 5: Run tests and syntax checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
php -l includes/post-types.php
|
||||
php -l templates/single-event.php
|
||||
php -l templates/archive-event.php
|
||||
```
|
||||
|
||||
Expected: tests pass and every PHP file reports no syntax errors.
|
||||
|
||||
- [ ] **Step 6: Commit consumer updates when Git is available**
|
||||
|
||||
```bash
|
||||
git add includes/post-types.php templates/single-event.php templates/archive-event.php
|
||||
git commit -m "refactor: render normalized event occurrences"
|
||||
```
|
||||
|
||||
### Task 6: Render the Desktop Month Grid and Mobile Agenda
|
||||
|
||||
**Files:**
|
||||
- Modify: `templates/calendar.php`
|
||||
|
||||
- [ ] **Step 1: Replace per-day event collection**
|
||||
|
||||
Retain validated navigation parameters and localized weekday construction. Replace `$events_by_day` with `$calendar_occurrences`. For each published event, append every normalized occurrence that intersects `$month['month_start']` through `$month['month_end']`, adding `title`, `permalink`, and `post_id` to the normalized array.
|
||||
|
||||
Sort the mobile list with start timestamp, then title:
|
||||
|
||||
```php
|
||||
usort(
|
||||
$calendar_occurrences,
|
||||
function ( $a, $b ) {
|
||||
$comparison = aa_events_occurrence_start( $a ) <=> aa_events_occurrence_start( $b );
|
||||
return 0 !== $comparison ? $comparison : strcasecmp( $a['title'], $b['title'] );
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Pass the occurrences into `aa_events_group_calendar_weeks()` for desktop rendering.
|
||||
|
||||
- [ ] **Step 2: Render semantic desktop week grids**
|
||||
|
||||
Replace the event-bearing table body with `.aa-events-calendar-grid`, while retaining a visually aligned weekday header. For each week render:
|
||||
|
||||
```php
|
||||
<div class="aa-events-calendar-week" role="row" style="--aa-event-lanes: <?php echo esc_attr( max( 1, $week['lane_count'] ) ); ?>">
|
||||
<div class="aa-events-calendar-days">
|
||||
<?php foreach ( $week['days'] as $day ) : ?>
|
||||
<div role="gridcell" class="aa-events-calendar-day ..." aria-label="...">
|
||||
<span class="day-number"><?php echo esc_html( $day->format( 'j' ) ); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="aa-events-calendar-bars">
|
||||
<!-- segment links -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Render every segment link with CSS variables `--aa-event-column`, `--aa-event-span`, and `--aa-event-lane`. Add `is-continuing-before` and `is-continuing-after` classes. Repeat the visible title on every segment and build `aria-label` from title plus the complete `aa_events_format_occurrence()` result. Mark visible arrows `aria-hidden="true"`.
|
||||
|
||||
Days outside the selected month remain visible as muted grid cells so week geometry stays intact. Keep the current-day class based on site timezone.
|
||||
|
||||
- [ ] **Step 3: Render mobile agenda markup**
|
||||
|
||||
After the desktop grid, render `.aa-events-calendar-agenda` with a heading and `<ol>`. Each intersecting occurrence appears once:
|
||||
|
||||
```php
|
||||
<li class="aa-events-agenda-item">
|
||||
<a href="<?php echo esc_url( $occurrence['permalink'] ); ?>">
|
||||
<span class="aa-events-agenda-title"><?php echo esc_html( $occurrence['title'] ); ?></span>
|
||||
<time datetime="<?php echo esc_attr( aa_events_occurrence_start( $occurrence )->format( DATE_W3C ) ); ?>">
|
||||
<?php echo esc_html( aa_events_format_occurrence( $occurrence ) ); ?>
|
||||
</time>
|
||||
</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
Render a localized “No events this month.” message instead of an empty list.
|
||||
|
||||
- [ ] **Step 4: Remove dead output-buffer code**
|
||||
|
||||
Delete `$originalEchoDayCell`, `$calendarRows`, `$calendarOutput`, and the unused `preg_replace()` block. Confirm no output buffering remains in the template.
|
||||
|
||||
- [ ] **Step 5: Run syntax and helper tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
php -l templates/calendar.php
|
||||
```
|
||||
|
||||
Expected: tests pass and calendar template has no syntax errors.
|
||||
|
||||
- [ ] **Step 6: Commit calendar markup when Git is available**
|
||||
|
||||
```bash
|
||||
git add templates/calendar.php
|
||||
git commit -m "feat: render spanning calendar event bars"
|
||||
```
|
||||
|
||||
### Task 7: Style Spanning Bars and the Mobile Agenda
|
||||
|
||||
**Files:**
|
||||
- Modify: `assets/css/aa-events.css`
|
||||
- Modify: `templates/aa-events.css`
|
||||
|
||||
- [ ] **Step 1: Replace table-only calendar styling**
|
||||
|
||||
Implement these layout contracts in `assets/css/aa-events.css`:
|
||||
|
||||
```css
|
||||
.aa-events-calendar-grid { border-left: 1px solid #aaa; border-top: 1px solid #aaa; }
|
||||
.aa-events-calendar-week { position: relative; }
|
||||
.aa-events-calendar-days,
|
||||
.aa-events-calendar-weekdays { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
||||
.aa-events-calendar-day {
|
||||
border-bottom: 1px solid #aaa;
|
||||
border-right: 1px solid #aaa;
|
||||
min-height: calc(4.5rem + var(--aa-event-lanes, 1) * 2rem);
|
||||
padding: .625rem;
|
||||
}
|
||||
.aa-events-calendar-bars {
|
||||
display: grid;
|
||||
gap: .25rem 0;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(var(--aa-event-lanes), 1.75rem);
|
||||
left: 0;
|
||||
padding: 2.25rem .2rem .5rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.aa-events-calendar-event {
|
||||
background: #0969da;
|
||||
border-radius: .25rem;
|
||||
color: #fff;
|
||||
grid-column: calc(var(--aa-event-column) + 1) / span var(--aa-event-span);
|
||||
grid-row: calc(var(--aa-event-lane) + 1);
|
||||
overflow: hidden;
|
||||
padding: .2rem .45rem;
|
||||
pointer-events: auto;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.aa-events-calendar-event.is-continuing-before { border-bottom-left-radius: 0; border-top-left-radius: 0; }
|
||||
.aa-events-calendar-event.is-continuing-after { border-bottom-right-radius: 0; border-top-right-radius: 0; }
|
||||
.aa-events-calendar-event:focus-visible { outline: 3px solid #111; outline-offset: 2px; z-index: 2; }
|
||||
.aa-events-calendar-day.is-outside-month { background: #f6f7f7; color: #646970; }
|
||||
.aa-events-calendar-day.today-cell { background: #ffe9b3; box-shadow: inset 0 0 0 2px #f7b500; }
|
||||
.aa-events-calendar-agenda { display: none; }
|
||||
```
|
||||
|
||||
Adjust the week minimum height from `--aa-event-lanes` so absolute bars cannot overlap the following week. Preserve existing header, Today button, archive, and card-grid styles.
|
||||
|
||||
- [ ] **Step 2: Add the mobile breakpoint**
|
||||
|
||||
At `max-width: 640px`, hide `.aa-events-calendar-desktop`, show `.aa-events-calendar-agenda`, and style agenda links as full-width blocks with a visible left border, readable title/date hierarchy, and `:focus-visible`. Do not rely on color alone to identify links.
|
||||
|
||||
- [ ] **Step 3: Synchronize the theme-copy stylesheet**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cp assets/css/aa-events.css templates/aa-events.css
|
||||
cmp assets/css/aa-events.css templates/aa-events.css
|
||||
```
|
||||
|
||||
Expected: `cmp` exits with status 0.
|
||||
|
||||
- [ ] **Step 4: Verify CSS structure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "aa-events-calendar-(grid|week|event|agenda)" assets/css/aa-events.css
|
||||
cmp assets/css/aa-events.css templates/aa-events.css
|
||||
```
|
||||
|
||||
Expected: all four component selectors are present and both stylesheets are identical.
|
||||
|
||||
- [ ] **Step 5: Commit responsive styling when Git is available**
|
||||
|
||||
```bash
|
||||
git add assets/css/aa-events.css templates/aa-events.css
|
||||
git commit -m "style: add responsive multi-day calendar layout"
|
||||
```
|
||||
|
||||
### Task 8: Document the New Editing and Theme Behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Rewrite the multiple-date documentation**
|
||||
|
||||
Rename “Multiple Event Dates” to “Event Occurrences.” Document that each row supports a required start date, optional times/end date, and all-day mode. Include one example combining a Friday–Sunday occurrence with a separate later occurrence.
|
||||
|
||||
- [ ] **Step 2: Document responsive calendar behavior**
|
||||
|
||||
State explicitly that desktop/tablet retain the traditional month grid with week-spanning bars and screens at or below 640px use an agenda list.
|
||||
|
||||
- [ ] **Step 3: Document custom ACF groups and theme overrides**
|
||||
|
||||
List required repeater/subfield names: `event_dates`, `date`, `start_time`, `end_date`, `end_time`, `all_day`. Explain that an existing theme override of `aa-events/calendar.php` remains authoritative and must be updated to opt into spanning bars, while it can use `aa_events_get_occurrences()` immediately.
|
||||
|
||||
- [ ] **Step 4: Check documentation terminology**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "multiple_dates|Event Dates repeater|multi-day|Occurrences|640px" README.md
|
||||
```
|
||||
|
||||
Expected: no instructions tell editors to use `multiple_dates`; occurrence and responsive behavior are documented.
|
||||
|
||||
- [ ] **Step 5: Commit documentation when Git is available**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs: explain event occurrence ranges"
|
||||
```
|
||||
|
||||
### Task 9: Final Verification
|
||||
|
||||
**Files:**
|
||||
- Verify all modified PHP, JavaScript, CSS, tests, templates, and documentation.
|
||||
|
||||
- [ ] **Step 1: Run the complete automated suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
find . -path './.superpowers' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l
|
||||
cmp assets/css/aa-events.css templates/aa-events.css
|
||||
```
|
||||
|
||||
Expected: all AA Events tests pass, every PHP file reports no syntax errors, and CSS files compare equal.
|
||||
|
||||
- [ ] **Step 2: Verify field-name consistency**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "multiple_dates" --glob '!docs/IMPLEMENTATION_PLAN_REPEATER.md' .
|
||||
rg -n "event_dates|end_date|end_time|all_day" includes templates README.md
|
||||
```
|
||||
|
||||
Expected: no production references to `multiple_dates`; all new fields are present in registration, normalization, templates, and documentation.
|
||||
|
||||
- [ ] **Step 3: Perform WordPress editor checks**
|
||||
|
||||
In the local WordPress site:
|
||||
|
||||
1. Create one event with a timed Friday 6 PM through Sunday 2 PM occurrence.
|
||||
2. Add a second all-day occurrence to the same event.
|
||||
3. Confirm all-day disables/clears time inputs.
|
||||
4. Attempt an end before the start and confirm a row-numbered save error.
|
||||
5. Save a legacy-only event and confirm exactly one occurrence row is created.
|
||||
6. Save it again and confirm no duplicate row appears.
|
||||
|
||||
Expected: editing, validation, and migration match the design.
|
||||
|
||||
- [ ] **Step 4: Perform calendar and accessibility checks**
|
||||
|
||||
1. View a month where an occurrence crosses a week and confirm the title repeats on both segments.
|
||||
2. View occurrences crossing previous/next month boundaries and confirm continuation indicators.
|
||||
3. Create overlapping ranges and confirm separate lanes with no covering.
|
||||
4. Change WordPress “Week Starts On” between Sunday and Monday and confirm segment columns remain correct.
|
||||
5. Keyboard-tab through event bars and confirm visible focus and complete accessible labels.
|
||||
6. At 641px confirm the traditional grid; at 640px confirm the agenda and one item per occurrence.
|
||||
7. Confirm Today and previous/next navigation update both presentations.
|
||||
|
||||
Expected: all visual, responsive, and keyboard behaviors match the approved specification.
|
||||
|
||||
- [ ] **Step 5: Commit final corrections when Git is available**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: complete multi-day occurrence verification"
|
||||
```
|
||||
|
||||
Only create this commit if verification required corrections; otherwise leave the previous focused commits as the complete history.
|
||||
@@ -1,64 +0,0 @@
|
||||
# ACF Field-Key Switching Compatibility 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:** Make event occurrence rows load and remain editable when switching between the built-in and MNBC local ACF field groups.
|
||||
|
||||
**Architecture:** Resolve schedule values through the active ACF field definition so stale hidden reference keys cannot block reads. Add an idempotent legacy repeater migration, then synchronize the existing MNBC JSON group through ACF's import API and migrate live events without deleting old metadata.
|
||||
|
||||
**Tech Stack:** PHP 7.4+, WordPress, ACF PRO, WP-CLI, custom PHP test harness
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Load Through the Active ACF Definition
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/bootstrap.php`
|
||||
- Modify: `tests/test-occurrences.php`
|
||||
- Modify: `includes/occurrences.php`
|
||||
|
||||
- [ ] Add test doubles for `acf_get_field()`, `acf_get_value()`, and
|
||||
`acf_format_value()` that can model an active field definition.
|
||||
- [ ] Add a regression test where normal `get_field('event_dates')` returns a
|
||||
raw repeater count but the active ACF definition returns rows.
|
||||
- [ ] Run `php tests/run.php` and verify the regression fails.
|
||||
- [ ] Add `aa_events_get_active_field_value()` and use it for
|
||||
`event_dates`/`multiple_dates` reads.
|
||||
- [ ] Run `php tests/run.php` and verify all assertions pass.
|
||||
|
||||
### Task 2: Migrate Legacy Repeater Rows
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test-occurrences.php`
|
||||
- Modify: `includes/occurrences.php`
|
||||
|
||||
- [ ] Add tests proving a legacy-only event writes canonical rows, an event
|
||||
with canonical rows is untouched, and legacy metadata is not deleted.
|
||||
- [ ] Run `php tests/run.php` and verify the new assertions fail.
|
||||
- [ ] Implement `aa_events_migrate_multiple_dates_occurrences()` and invoke it
|
||||
from the existing event-save migration hook before legacy scalar migration.
|
||||
- [ ] Run `php tests/run.php` and verify all assertions pass.
|
||||
|
||||
### Task 3: Synchronize and Migrate the Live Site
|
||||
|
||||
**Files:**
|
||||
- Read: `../../themes/MNBC/acf/group_69fb58894f83f.json`
|
||||
- Mutate: the local WordPress ACF field-group database records through WP-CLI
|
||||
|
||||
- [ ] Import the decoded JSON with `acf_import_field_group()`.
|
||||
- [ ] Run the legacy repeater migration for every event post.
|
||||
- [ ] Verify the runtime local repeater is named `event_dates`.
|
||||
- [ ] Verify event 415 loads canonical occurrence rows and retain a snapshot of
|
||||
its legacy schedule metadata for comparison.
|
||||
|
||||
### Task 4: Final Verification
|
||||
|
||||
**Files:**
|
||||
- Verify: `includes/occurrences.php`
|
||||
- Verify: `tests/bootstrap.php`
|
||||
- Verify: `tests/test-occurrences.php`
|
||||
|
||||
- [ ] Run PHP syntax checks on all changed PHP files.
|
||||
- [ ] Run `php tests/run.php`.
|
||||
- [ ] Run a read-only live WP-CLI check for field definitions, event 415
|
||||
occurrence source, and schedule metadata.
|
||||
@@ -1,160 +0,0 @@
|
||||
# Local ACF Override Compatibility 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:** Keep existing MNBC events visible with the legacy local ACF override while normalizing that override for future edits.
|
||||
|
||||
**Architecture:** Extend occurrence input normalization at the plugin boundary, then add `multiple_dates` as a lower-priority repeater source. Extend metadata synchronization to observe that legacy source. Finally, update the theme ACF JSON to the canonical AA Events names and formats without changing stable field keys or migrating stored metadata.
|
||||
|
||||
**Tech Stack:** PHP 7.4+, WordPress, ACF PRO, JSON, the plugin's custom PHP test harness
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Parse Existing Override Values
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test-occurrences.php`
|
||||
- Modify: `includes/occurrences.php`
|
||||
|
||||
- [ ] **Step 1: Write failing parser regression tests**
|
||||
|
||||
Add assertions showing that `aa_events_normalize_occurrence_row()` accepts
|
||||
`July 30, 2026` and `2:15 pm`, normalizes them to `2026-07-30` and
|
||||
`14:15:00`, and rejects invalid calendar/time strings.
|
||||
|
||||
- [ ] **Step 2: Run the focused test and verify RED**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: failures report that the formatted override row was not normalized.
|
||||
|
||||
- [ ] **Step 3: Implement minimal multi-format parsing**
|
||||
|
||||
Add focused date and time parsing helpers. Dates must try `Y-m-d`, `Ymd`, and
|
||||
`F j, Y` with exact round-trip validation. Times must try `H:i:s`, `H:i`, and
|
||||
`g:i a`, normalizing accepted values to `H:i:s`. Route occurrence-row date
|
||||
parsing through the date helper while retaining existing strict validation.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: all assertions pass.
|
||||
|
||||
### Task 2: Read Legacy `multiple_dates` Rows
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test-occurrences.php`
|
||||
- Modify: `includes/occurrences.php`
|
||||
|
||||
- [ ] **Step 1: Write failing source-precedence tests**
|
||||
|
||||
Create an event containing only `multiple_dates` formatted rows and assert that
|
||||
`aa_events_get_occurrences()` returns normalized occurrences. Create another
|
||||
event containing both repeaters and assert that `event_dates` is authoritative.
|
||||
Also assert the formatted standalone `date`/`start_time` fallback works.
|
||||
|
||||
- [ ] **Step 2: Run the focused test and verify RED**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: the `multiple_dates` event returns no occurrence.
|
||||
|
||||
- [ ] **Step 3: Implement repeater fallback**
|
||||
|
||||
Read `event_dates` first. Only when it is empty, read `multiple_dates`. Feed
|
||||
either repeater through the existing row normalization path and preserve the
|
||||
existing public `event_dates` source label.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: all assertions pass.
|
||||
|
||||
### Task 3: Synchronize Legacy Repeater Metadata
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test-occurrences.php`
|
||||
- Modify: `includes/occurrences.php`
|
||||
- Modify: `includes/post-types.php`
|
||||
|
||||
- [ ] **Step 1: Write failing synchronization tests**
|
||||
|
||||
Assert that `aa_events_sort_source_meta_key()` accepts `multiple_dates`, its
|
||||
supported row subfield keys, and rejects unrelated keys. Queue a legacy-row
|
||||
change and assert that `aa_events_run_queued_sort_sync()` flushes the
|
||||
`multiple_dates` ACF cache and writes the canonical sort metadata.
|
||||
|
||||
- [ ] **Step 2: Run the focused test and verify RED**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: the legacy key is ignored or its ACF cache is not flushed.
|
||||
|
||||
- [ ] **Step 3: Extend metadata recognition and SQL fallback**
|
||||
|
||||
Add `multiple_dates` to exact-key checks, row-key patterns, and cache-flush
|
||||
lists. Extend the unsynchronized SQL sorting fallback to consider
|
||||
`multiple_dates_N_date` rows after canonical `event_dates` rows while keeping
|
||||
the canonical indexed metadata first.
|
||||
|
||||
- [ ] **Step 4: Run tests and verify GREEN**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: all assertions pass.
|
||||
|
||||
### Task 4: Normalize the MNBC ACF Field Group
|
||||
|
||||
**Files:**
|
||||
- Modify: `../../themes/MNBC/acf/group_69fb58894f83f.json`
|
||||
|
||||
- [ ] **Step 1: Update only the schedule contract**
|
||||
|
||||
Keep all field keys stable. Change the repeater name from `multiple_dates` to
|
||||
`event_dates`; change both date-picker return formats to `Y-m-d`; change
|
||||
the standalone and repeater start/end time return formats to `H:i:s`.
|
||||
|
||||
- [ ] **Step 2: Validate the JSON and contract**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php -r '$p="../../themes/MNBC/acf/group_69fb58894f83f.json"; $j=json_decode(file_get_contents($p), true, 512, JSON_THROW_ON_ERROR); $fields=$j["fields"]; $repeater=array_values(array_filter($fields, fn($f) => ($f["key"] ?? "") === "field_69fb5b1aac54a"))[0]; if (($repeater["name"] ?? "") !== "event_dates") { exit(1); } echo "valid\n";'
|
||||
```
|
||||
|
||||
from `wp-content/plugins/AA-Events`.
|
||||
|
||||
Expected: `valid`
|
||||
|
||||
### Task 5: Complete Verification
|
||||
|
||||
**Files:**
|
||||
- Verify: `includes/occurrences.php`
|
||||
- Verify: `includes/post-types.php`
|
||||
- Verify: `tests/test-occurrences.php`
|
||||
- Verify: `../../themes/MNBC/acf/group_69fb58894f83f.json`
|
||||
|
||||
- [ ] **Step 1: Lint changed PHP**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
php -l includes/occurrences.php
|
||||
php -l includes/post-types.php
|
||||
php -l tests/test-occurrences.php
|
||||
```
|
||||
|
||||
Expected: no syntax errors.
|
||||
|
||||
- [ ] **Step 2: Run the complete test suite**
|
||||
|
||||
Run: `php tests/run.php`
|
||||
|
||||
Expected: `All tests passed.`
|
||||
|
||||
- [ ] **Step 3: Review the final diff without Git**
|
||||
|
||||
Because this installation is not a Git worktree, inspect the exact changed
|
||||
sections and confirm that no unrelated theme fields or plugin behavior changed.
|
||||
@@ -1,919 +0,0 @@
|
||||
# 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 existing `1.2.1` plugin header.
|
||||
- Modify `tests/test-admin-consumers.php`: align its expected release version with `1.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:
|
||||
|
||||
```php
|
||||
$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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```php
|
||||
<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:
|
||||
|
||||
```php
|
||||
<div class="aa-events-calendar-desktop" data-aa-events-view-panel="calendar">
|
||||
```
|
||||
|
||||
```php
|
||||
<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:
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
node --test tests/test-calendar-view.js
|
||||
```
|
||||
|
||||
Expected: 9 tests pass.
|
||||
|
||||
- [ ] **Step 5: Run a JavaScript syntax check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
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
|
||||
<?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`:
|
||||
|
||||
```php
|
||||
require_once __DIR__ . '/test-frontend-assets.php';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the PHP suite and verify the enqueue assertion fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Enqueue frontend assets.
|
||||
*/
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Align the existing release version**
|
||||
|
||||
In `aa-events.php`, change:
|
||||
|
||||
```php
|
||||
define( 'AA_EVENTS_VERSION', '1.2.1' );
|
||||
```
|
||||
|
||||
In `tests/test-admin-consumers.php`, change the hard-coded release assertion to:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```bash
|
||||
php tests/run.php
|
||||
```
|
||||
|
||||
Expected: `All AA Events tests passed.`
|
||||
|
||||
- [ ] **Step 7: Commit the integration slice when Git is available**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```markdown
|
||||
- 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:
|
||||
|
||||
```markdown
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
1. At a width above 640 pixels, confirm Calendar is initially selected.
|
||||
2. Use the keyboard to select List and confirm focus remains visible.
|
||||
3. Navigate to the next month and reload; confirm List remains selected.
|
||||
4. Return to Calendar and confirm that preference persists.
|
||||
5. Resize to exactly 640 pixels; confirm the switcher and grid are hidden and List is visible.
|
||||
6. Resize above 640 pixels; confirm the saved desktop choice returns.
|
||||
7. Render two calendars on one page if the host theme permits it; selecting either switcher must update both.
|
||||
8. 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**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs: explain persistent calendar views"
|
||||
```
|
||||
|
||||
In the current non-Git workspace, record the commit as skipped.
|
||||
Reference in New Issue
Block a user