diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 46a3e42..6a9b3ee 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -41,7 +41,6 @@ jobs: - name: Run E2E tests env: - WP_BASE_URL: http://localhost:8888 WP_USERNAME: admin WP_PASSWORD: password run: npm run test:e2e diff --git a/.gitignore b/.gitignore index e9d431e..6654761 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ build/* !build/index.php +# wp-env local overrides (per-machine ports etc. — never commit) +.wp-env.override.json + # Playwright test-results/ playwright-report/ diff --git a/README.md b/README.md index 235ef01..9f8f2d2 100644 --- a/README.md +++ b/README.md @@ -186,3 +186,12 @@ Simple plugin to add a focal point control to the featured post image. **If you want to use this plugin extension**, you can find it at https://github.com/a8cteam51/bamberg-ua/tree/trunk/mu-plugins/team51-focal-point Copy the `team51-focal-point` folder to your `mu-plugins` directory. + +## Changelog + +### 2.1.1 + +- **Loop Event Info block:** added a configurable HTML wrapper element (`div`/`p`/`h1`–`h6`), per-block date and time format overrides, and a query offset control. +- **Query Loop Events:** the block-editor preview now matches the published front-end for every feed order. Previously, with "Newest to Oldest" the editor preview showed the oldest events while the front-end showed the newest. +- Removed a redundant query cache-buster that wrote a timestamp into post content on every change. +- Added Playwright end-to-end tests (run in CI) covering the event-dates save flow, the Query Loop editor/front-end parity, and the Loop Event Info rendering options. diff --git a/package-lock.json b/package-lock.json index b89fce2..f97514f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wpcomsp-simple-events", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wpcomsp-simple-events", - "version": "2.1.0", + "version": "2.1.1", "license": "GPL-2.0-or-later", "dependencies": { "ajv": "^8.17.1" diff --git a/package.json b/package.json index baf38ef..0b06d50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wpcomsp-simple-events", - "version": "2.1.0", + "version": "2.1.1", "description": "A simple Gutenberg-first event management plugin that integrates with WooCommerce Box Office.", "author": { "name": "WordPress.com Special Projects Team", @@ -48,6 +48,7 @@ "lint:pkg-json": "wp-scripts lint-pkg-json", "packages-update": "wp-scripts packages-update", "test:e2e": "playwright test --config=tests/e2e/playwright.config.js", + "test:e2e:local": "wp-env start && playwright test --config=tests/e2e/playwright.config.js", "test:e2e:ui": "playwright test --config=tests/e2e/playwright.config.js --ui", "test:e2e:headed": "playwright test --config=tests/e2e/playwright.config.js --headed", "test:e2e:debug": "playwright test --config=tests/e2e/playwright.config.js --debug", diff --git a/plugin.php b/plugin.php index c8b7ff3..4a949fc 100644 --- a/plugin.php +++ b/plugin.php @@ -3,7 +3,7 @@ * Simple Events Plugin bootstrap file. * * @since 1.0.0 - * @version 2.1.0 + * @version 2.1.1 * @author WordPress.com Special Projects * @license GPL-3.0-or-later * @@ -14,7 +14,7 @@ * Description: Event management frontend for WooCommerce Box Office. * Requires at least: 6.5 * Tested up to: 6.9 - * Version: 2.1.0 + * Version: 2.1.1 * Requires PHP: 8.0 * Author: WordPress.com Special Projects * Author URI: https://wpspecialprojects.wordpress.com @@ -32,7 +32,7 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/includes/plugin.php'; define( 'SE_METADATA', get_plugin_data( __FILE__, false, false ) ); -define( 'SE_VERSION', '2.1.0' ); +define( 'SE_VERSION', '2.1.1' ); define( 'SE_BASENAME', plugin_basename( __FILE__ ) ); define( 'SE_PLUGIN_DIR', untrailingslashit( plugin_dir_path( __FILE__ ) ) ); define( 'SE_PLUGIN_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) ); diff --git a/src/blocks/loop-event-info/block.json b/src/blocks/loop-event-info/block.json index cf16fce..bf35b5e 100644 --- a/src/blocks/loop-event-info/block.json +++ b/src/blocks/loop-event-info/block.json @@ -36,6 +36,19 @@ "order": { "type": "string", "default": "asc" + }, + "dateFormat": { + "type": "string", + "default": "" + }, + "timeFormat": { + "type": "string", + "default": "" + }, + "tagName": { + "enum": ["div", "p", "h1", "h2", "h3", "h4", "h5", "h6"], + "type": "string", + "default": "div" } }, "supports": { diff --git a/src/blocks/loop-event-info/index.js b/src/blocks/loop-event-info/index.js index 1722c44..959ebc9 100644 --- a/src/blocks/loop-event-info/index.js +++ b/src/blocks/loop-event-info/index.js @@ -2,7 +2,7 @@ import './index.scss'; import './editor.scss'; import metadata from './block.json'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { registerBlockType } from '@wordpress/blocks'; import { PanelBody, @@ -19,9 +19,27 @@ import { } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; +import { dateI18n, getSettings as getDateSettings } from '@wordpress/date'; registerBlockType(metadata, { - edit: ({ attributes: { metaName, metaPrefix, thePostId, textAlign, addCalendarLinks, feedType, order }, setAttributes, context: { postId }, clientId }) => { + edit: ({ attributes: { metaName, metaPrefix, thePostId, textAlign, addCalendarLinks, feedType, order, dateFormat, timeFormat, tagName }, setAttributes, context: { postId }, clientId }) => { + + const siteFormats = getDateSettings().formats; + const siteDateFormat = siteFormats.date; + const siteTimeFormat = siteFormats.time; + const showDateFormat = metaName === 'dates' || metaName === 'date'; + const showTimeFormat = metaName === 'dates' || metaName === 'time'; + + const formatPreview = (format) => { + if (!format) { + return ''; + } + try { + return dateI18n(format, new Date()); + } catch (e) { + return ''; + } + }; // Get query loop data from our custom store const queryData = useSelect((select) => { @@ -57,6 +75,24 @@ registerBlockType(metadata, { + + setAttributes({ tagName: value }) + } + __nextHasNoMarginBottom + /> + { showDateFormat && ( + + setAttributes({ dateFormat: value }) + } + __nextHasNoMarginBottom + /> + ) } + { showTimeFormat && ( + + setAttributes({ timeFormat: value }) + } + __nextHasNoMarginBottom + /> + ) } @@ -108,6 +192,9 @@ registerBlockType(metadata, { addCalendarLinks, feedType, // Use block attribute values order, // Use block attribute values + dateFormat, + timeFormat, + tagName, }} /> diff --git a/src/classes/class-date-display-formatter.php b/src/classes/class-date-display-formatter.php index 8dce434..0be5a3e 100644 --- a/src/classes/class-date-display-formatter.php +++ b/src/classes/class-date-display-formatter.php @@ -118,6 +118,20 @@ class SE_Date_Display_Formatter { */ private $use_html_in_date_output = false; + /** + * Override for the date format. Empty string means use the site option. + * + * @var string + */ + private $date_format = ''; + + /** + * Override for the time format. Empty string means use the site option. + * + * @var string + */ + private $time_format = ''; + /** * Create a new instance of the date display formatter. * @@ -169,6 +183,28 @@ public function set_time_only( bool $time_only = true ) { $this->time_only = $time_only; } + /** + * Override the date format used by format_date(). Empty string restores the site default. + * + * @param string $format A PHP date format string. + * + * @return void + */ + public function set_date_format( string $format ): void { + $this->date_format = $format; + } + + /** + * Override the time format used by format_time(). Empty string restores the site default. + * + * @param string $format A PHP date format string. + * + * @return void + */ + public function set_time_format( string $format ): void { + $this->time_format = $format; + } + /** * Modify Timezone. * @@ -630,7 +666,8 @@ private function get_timezone_abbreviation() { * @return string */ public function format_date( $date_timestamp ) { - return wp_date( get_option( 'date_format' ), $date_timestamp, $this->get_timezone_instance() ); + $format = '' !== $this->date_format ? $this->date_format : get_option( 'date_format' ); + return wp_date( $format, $date_timestamp, $this->get_timezone_instance() ); } /** @@ -641,7 +678,8 @@ public function format_date( $date_timestamp ) { * @return string */ public function format_time( $time_timestamp ) { - return wp_date( get_option( 'time_format' ), $time_timestamp, $this->get_timezone_instance() ); + $format = '' !== $this->time_format ? $this->time_format : get_option( 'time_format' ); + return wp_date( $format, $time_timestamp, $this->get_timezone_instance() ); } /** diff --git a/src/classes/class-se-block-variations.php b/src/classes/class-se-block-variations.php index ff0361c..e0c5070 100644 --- a/src/classes/class-se-block-variations.php +++ b/src/classes/class-se-block-variations.php @@ -127,7 +127,27 @@ public function modify_event_posts( $posts, $query ) { public function set_admin_query( $args, $request ) { $feed_type = $request->get_param( 'feedType' ); - $feed_order = $request->get_param( 'order' );# + $feed_order = $request->get_param( 'order' ); + + // This filter runs for EVERY rest_se-event_query request. `feedType` + // is a custom param only the events Query Loop variation sends; a + // plain /wp/v2/se-event request never has it. Bail untouched for + // those so generic REST consumers (post lists, integrations) aren't + // switched to the child post type. (`order` is a standard REST + // collection param present on every request, so it can't be the + // discriminator.) + if ( null === $feed_type ) { + return $args; + } + + // Mirror build_query: run the events query against the child + // se-event-date posts (which carry se_event_date_start/end); + // modify_event_posts remaps them back to parent events. Without + // this the editor REST preview queries se-event parents that lack + // the date meta, so the meta-order SQL never gets its `+0 ASC` + // form and fix_sort_order can't flip it — the editor preview was + // stuck oldest-first regardless of feed order. + $args['post_type'] = SE_Event_Post_Type::$event_date_post_type; return $this->set_event_query_args( $args, $feed_type, $feed_order ); } diff --git a/src/classes/class-se-blocks.php b/src/classes/class-se-blocks.php index 2900970..14a3c12 100644 --- a/src/classes/class-se-blocks.php +++ b/src/classes/class-se-blocks.php @@ -842,6 +842,9 @@ public static function loop_event_info_render( $attributes, $content, $block ): break; } + $date_format = isset( $attributes['dateFormat'] ) ? (string) $attributes['dateFormat'] : ''; + $time_format = isset( $attributes['timeFormat'] ) ? (string) $attributes['timeFormat'] : ''; + // Generate output based on meta name. if ( ! empty( $post_ID ) ) { switch ( $attributes['metaName'] ) { @@ -852,13 +855,13 @@ public static function loop_event_info_render( $attributes, $content, $block ): $output = se_event_get_venue( $post_ID ); break; case 'dates': - $output = $get_date_function( $post_ID, $event_date_id ); + $output = $get_date_function( $post_ID, $event_date_id, false, false, null, $date_format, $time_format ); break; case 'date': - $output = $get_date_function( $post_ID, $event_date_id, true, false ); + $output = $get_date_function( $post_ID, $event_date_id, true, false, null, $date_format, $time_format ); break; case 'time': - $output = $get_date_function( $post_ID, $event_date_id, false, true ); + $output = $get_date_function( $post_ID, $event_date_id, false, true, null, $date_format, $time_format ); break; } } @@ -890,9 +893,15 @@ public static function loop_event_info_render( $attributes, $content, $block ): $output .= se_template_calendar_links( false ); } + $allowed_tags = array( 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ); + $tag_name = isset( $attributes['tagName'] ) && in_array( $attributes['tagName'], $allowed_tags, true ) + ? $attributes['tagName'] + : 'div'; + // Add gutenberg generated wrapper atts. $output = sprintf( - '
%s%s
', + '<%1$s %2$s>%3$s%4$s', + $tag_name, get_block_wrapper_attributes( array( 'class' => 'has-text-align-' . esc_attr( $attributes['textAlign'] ), diff --git a/src/event-functions.php b/src/event-functions.php index 65029a1..cc8bb20 100644 --- a/src/event-functions.php +++ b/src/event-functions.php @@ -159,10 +159,12 @@ function ( $date ) { * @param boolean $date_only Whether to return only the date. * @param boolean $time_only Whether to return only the time. * @param array $event_dates Event dates. + * @param string $date_format Optional date format override. Empty string uses the site default. + * @param string $time_format Optional time format override. Empty string uses the site default. * * @return string */ -function se_event_get_future_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null ) { +function se_event_get_future_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null, $date_format = '', $time_format = '' ) { $date_display_formatter = new SE_Date_Display_Formatter( $event_id ); $now = SE_Calendar::get_instance()->create_date_time( 'now' )->format( 'U' ); @@ -173,6 +175,13 @@ function se_event_get_future_dates( $event_id, $event_date_id = null, $date_only $date_display_formatter->set_time_only( true ); } + if ( '' !== $date_format ) { + $date_display_formatter->set_date_format( $date_format ); + } + if ( '' !== $time_format ) { + $date_display_formatter->set_time_format( $time_format ); + } + // If we dont have any dates. if ( ! $event_dates ) { $event_dates = se_event_get_event_dates( $event_id ); @@ -214,10 +223,12 @@ function ( $date ) use ( $now ) { * @param boolean $date_only Whether to return only the date. * @param boolean $time_only Whether to return only the time. * @param array $event_dates Event dates. + * @param string $date_format Optional date format override. Empty string uses the site default. + * @param string $time_format Optional time format override. Empty string uses the site default. * * @return string */ -function se_event_get_past_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null ) { +function se_event_get_past_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null, $date_format = '', $time_format = '' ) { // Match the se_event_get_future_dates but for past dates $date_display_formatter = new SE_Date_Display_Formatter( $event_id ); @@ -229,6 +240,13 @@ function se_event_get_past_dates( $event_id, $event_date_id = null, $date_only = $date_display_formatter->set_time_only( true ); } + if ( '' !== $date_format ) { + $date_display_formatter->set_date_format( $date_format ); + } + if ( '' !== $time_format ) { + $date_display_formatter->set_time_format( $time_format ); + } + // If we dont have any dates. if ( ! $event_dates ) { $event_dates = se_event_get_event_dates( $event_id ); @@ -268,10 +286,12 @@ function ( $date ) use ( $now ) { * @param boolean $date_only Whether to return only the date. * @param boolean $time_only Whether to return only the time. * @param array $event_dates Event dates. + * @param string $date_format Optional date format override. Empty string uses the site default. + * @param string $time_format Optional time format override. Empty string uses the site default. * * @return string */ -function se_event_get_formatted_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null ) { +function se_event_get_formatted_dates( $event_id, $event_date_id = null, $date_only = false, $time_only = false, $event_dates = null, $date_format = '', $time_format = '' ) { $date_display_formatter = new SE_Date_Display_Formatter( $event_id ); @@ -299,6 +319,13 @@ function ( $date ) use ( $event_date_id ) { $date_display_formatter->set_time_only( true ); } + if ( '' !== $date_format ) { + $date_display_formatter->set_date_format( $date_format ); + } + if ( '' !== $time_format ) { + $date_display_formatter->set_time_format( $time_format ); + } + return $date_display_formatter->format_dates( $event_dates ); } diff --git a/src/variations/query-loop-events/block.js b/src/variations/query-loop-events/block.js index cf750c9..cb80fa5 100644 --- a/src/variations/query-loop-events/block.js +++ b/src/variations/query-loop-events/block.js @@ -66,7 +66,6 @@ registerBlockVariation('core/query', { inherit: false, inheritTaxQuery: true, feedType: 'default', - _cacheBuster: Date.now(), }, eventsPerPage: 6, }, @@ -92,6 +91,9 @@ const FeedTypeControl = ({ attributes, setAttributes, clientId }) => { const [eventsPerPage, setEventsPerPage] = useState( query.perPage || 6 ); + const [eventsOffset, setEventsOffset] = useState( + query.offset || 0 + ); // Store the query data so child blocks can access it useEffect(() => { @@ -145,7 +147,6 @@ let feedOrderOptions = getFeedOrderOptions(feedType); query: { ...query, feedType: value, - _cacheBuster: Date.now() }, }); }} @@ -160,7 +161,6 @@ let feedOrderOptions = getFeedOrderOptions(feedType); query: { ...query, order: value, - _cacheBuster: Date.now() }, }); }} @@ -175,7 +175,6 @@ let feedOrderOptions = getFeedOrderOptions(feedType); query: { ...query, perPage: value, - _cacheBuster: Date.now() }, }); }} @@ -184,6 +183,28 @@ let feedOrderOptions = getFeedOrderOptions(feedType); step={1} __nextHasNoMarginBottom /> + { + const nextOffset = value || 0; + setEventsOffset(nextOffset); + setAttributes({ + query: { + ...query, + offset: nextOffset, + }, + }); + }} + min={0} + max={100} + step={1} + __nextHasNoMarginBottom + />

{__( 'Select the type of events to display and their order.', diff --git a/tests/e2e/.env.example b/tests/e2e/.env.example index 23c1a04..b8bfc81 100644 --- a/tests/e2e/.env.example +++ b/tests/e2e/.env.example @@ -1,5 +1,5 @@ -# Copy to .env and adjust for your local environment. -# Defaults match `npx wp-env start` out of the box. -WP_BASE_URL=http://localhost:8888 +# WP_BASE_URL is auto-discovered from the running wp-env (its assigned port +# can differ per machine/run). Only uncomment to force a specific URL. +# WP_BASE_URL=http://localhost:8888 WP_USERNAME=admin WP_PASSWORD=password diff --git a/tests/e2e/fixtures/seed-loop.php b/tests/e2e/fixtures/seed-loop.php new file mode 100644 index 0000000..1ea8a14 --- /dev/null +++ b/tests/e2e/fixtures/seed-loop.php @@ -0,0 +1,93 @@ + array( 'se-event', 'se-event-date' ), + 'post_status' => 'any', + 'numberposts' => -1, + ) +); +foreach ( $wipe as $p ) { + wp_delete_post( $p->ID, true ); +} +foreach ( get_posts( + array( + 'post_type' => 'page', + 'post_status' => 'any', + 'numberposts' => -1, + 's' => $prefix, + ) +) as $p ) { + wp_delete_post( $p->ID, true ); +} + +// Event-info block in content so the save_post cleanup hook keeps child dates. +$event_content = ''; + +foreach ( array( 10, 20, 30 ) as $i => $days ) { + $event_id = wp_insert_post( + array( + 'post_type' => 'se-event', + 'post_status' => 'publish', + 'post_title' => sprintf( '%s %s', $prefix, chr( 65 + $i ) ), + 'post_content' => $event_content, + ) + ); + + $ts = strtotime( "+{$days} days" ); + se_event_create_event_date( + $event_id, + array( + 'start_date' => $ts, + 'end_date' => $ts + 7200, + 'all_day' => false, + ) + ); +} + +// Real query-loop-events markup. offset:1 (skip the first of 3 → expect 2 +// rendered), loop-event-info set to date / h2 / Y-m-d override. +$markup = <<<'HTML' + +

+ + + + + + + +

No events

+ +
+ +HTML; + +$page_id = wp_insert_post( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => $prefix . ' PAGE', + 'post_content' => $markup, + ) +); + +echo (int) $page_id; diff --git a/tests/e2e/fixtures/seed-parity.php b/tests/e2e/fixtures/seed-parity.php new file mode 100644 index 0000000..9a61263 --- /dev/null +++ b/tests/e2e/fixtures/seed-parity.php @@ -0,0 +1,94 @@ + array( 'se-event', 'se-event-date' ), + 'post_status' => 'any', + 'numberposts' => -1, + ) +) as $p ) { + wp_delete_post( $p->ID, true ); +} +foreach ( get_posts( + array( + 'post_type' => 'page', + 'post_status' => 'any', + 'numberposts' => -1, + 's' => $prefix, + ) +) as $p ) { + wp_delete_post( $p->ID, true ); +} + +$event_content = ''; + +// 7 past + 7 future, wide spread, deliberately created in an order that +// does NOT match event-date order (so created-date vs event-date ordering +// diverge). Index = creation order; value = day offset from now. +$day_offsets = array( -60, 50, -10, 40, -45, 10, -30, 60, -5, 30, -50, 20, -20, 5 ); +foreach ( $day_offsets as $i => $days ) { + $event_id = wp_insert_post( + array( + 'post_type' => 'se-event', + 'post_status' => 'publish', + 'post_title' => sprintf( '%s %02d', $prefix, $i + 1 ), + 'post_content' => $event_content, + ) + ); + + $ts = strtotime( "{$days} days" ); + se_event_create_event_date( + $event_id, + array( + 'start_date' => $ts, + 'end_date' => $ts + 7200, + 'all_day' => false, + ) + ); +} + +// feedType default, perPage 20, order asc by date. +$markup = <<<'HTML' + +
+ + + + + + + +

No events

+ +
+ +HTML; + +$page_id = wp_insert_post( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => $prefix . ' PAGE', + 'post_content' => $markup, + ) +); + +echo (int) $page_id; diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js index e4cba21..cb8d45a 100644 --- a/tests/e2e/global-setup.js +++ b/tests/e2e/global-setup.js @@ -1,19 +1,17 @@ -const { test: setup, expect } = require( '@playwright/test' ); -const fs = require( 'fs' ); -const path = require( 'path' ); +const { test: setup } = require( '@wordpress/e2e-test-utils-playwright' ); -const STORAGE_PATH = path.join( __dirname, 'artifacts/storage-states/admin.json' ); -const USERNAME = process.env.WP_USERNAME || 'admin'; -const PASSWORD = process.env.WP_PASSWORD || 'password'; - -setup( 'authenticate as admin', async ( { page, baseURL } ) => { - await page.goto( `${ baseURL }/wp-login.php` ); - await page.fill( '#user_login', USERNAME ); - await page.fill( '#user_pass', PASSWORD ); - await page.click( '#wp-submit' ); - await page.waitForURL( /wp-admin/ ); - await expect( page.locator( '#wpadminbar' ) ).toBeVisible(); - - fs.mkdirSync( path.dirname( STORAGE_PATH ), { recursive: true } ); - await page.context().storageState( { path: STORAGE_PATH } ); +/** + * Authenticate once via the REST API and persist the storage state. + * + * Uses @wordpress/e2e-test-utils-playwright (the WordPress-official helper, + * same as the Pink-Crab Jukebox reference) instead of driving the wp-login + * form in a browser. REST auth is resilient to a freshly-started wp-env + * still warming up — the manual form-fill raced WP readiness and errored + * with chrome-error://chromewebdata/ on a cold start. + * + * Reads WP_BASE_URL / WP_USERNAME / WP_PASSWORD and writes the storage + * state to STORAGE_STATE_PATH (both set in playwright.config.js). + */ +setup( 'authenticate', async ( { requestUtils } ) => { + await requestUtils.setupRest(); } ); diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 0198735..6441ca1 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -1,8 +1,56 @@ const { defineConfig, devices } = require( '@playwright/test' ); +const { execSync } = require( 'child_process' ); const path = require( 'path' ); require( 'dotenv' ).config( { path: path.join( __dirname, '.env' ) } ); -const BASE_URL = process.env.WP_BASE_URL || 'http://localhost:8888'; +/** + * Resolve the WordPress base URL. + * + * wp-env assigns a different port per project/machine (and it can change), + * so a hardcoded fallback is wrong everywhere except by luck. Order of + * precedence: + * 1. WP_BASE_URL env / tests/e2e/.env (explicit override — CI sets this) + * 2. Ask the running wp-env what URL it's actually on (`wp option get home`) + * 3. wp-env's documented default, only if discovery fails + */ +function resolveBaseURL() { + if ( process.env.WP_BASE_URL ) { + return process.env.WP_BASE_URL; + } + try { + const url = execSync( + "npx wp-env run cli --env-cwd='wp-content/plugins/simple-events' -- wp option get home", + { cwd: path.join( __dirname, '../..' ), stdio: [ 'ignore', 'pipe', 'ignore' ] } + ) + .toString() + .trim() + .split( '\n' ) + .pop() + .trim(); + if ( /^https?:\/\//.test( url ) ) { + return url; + } + } catch ( e ) { + // wp-env not up yet / cli unavailable — fall through to default. + } + return 'http://localhost:8888'; +} + +const BASE_URL = resolveBaseURL(); +// Propagate so spec files (which read process.env.WP_BASE_URL directly) +// get the same discovered URL, not their own hardcoded fallback. +process.env.WP_BASE_URL = BASE_URL; + +// Where @wordpress/e2e-test-utils-playwright (global-setup) writes the +// authenticated storage state, and where the chromium project loads it. +const STORAGE_STATE_PATH = path.join( + __dirname, + 'artifacts/storage-states/admin.json' +); +process.env.STORAGE_STATE_PATH = STORAGE_STATE_PATH; +require( 'fs' ).mkdirSync( path.dirname( STORAGE_STATE_PATH ), { + recursive: true, +} ); module.exports = defineConfig( { testDir: __dirname, @@ -34,7 +82,7 @@ module.exports = defineConfig( { testMatch: /specs\/.*\.spec\.js$/, use: { ...devices[ 'Desktop Chrome' ], - storageState: path.join( __dirname, 'artifacts/storage-states/admin.json' ), + storageState: STORAGE_STATE_PATH, launchOptions: { slowMo: process.env.SLOWMO ? parseInt( process.env.SLOWMO, 10 ) : 0, }, diff --git a/tests/e2e/specs/editor/query-loop-parity.spec.js b/tests/e2e/specs/editor/query-loop-parity.spec.js new file mode 100644 index 0000000..175bf8b --- /dev/null +++ b/tests/e2e/specs/editor/query-loop-parity.spec.js @@ -0,0 +1,107 @@ +const { test, expect } = require( '@playwright/test' ); +const { execSync } = require( 'child_process' ); + +const BASE_URL = process.env.WP_BASE_URL || 'http://localhost:8888'; + +/** + * The editor preview of the events Query Loop MUST show the same events, + * in the same order, as the published front-end. Today it doesn't: + * + * - front-end (build_query) switches the query to the se-event-date child + * posts and orders by real event date. + * - editor preview (set_admin_query on rest_se-event_query) leaves the + * query on the se-event PARENT posts but orders by se_event_date_start, + * a meta key that only exists on the children → WP falls back to + * post/created-date order → a different, usually older, set. + * + * This asymmetry is pure server-side, so it reproduces on any Gutenberg + * version (unlike the feedType-param-forwarding quirk). The parity + * assertion below is RED on current code and is the spec for the fix. + */ +test.describe( 'Query Loop events – editor/front parity', () => { + let pageId; + + test.beforeAll( () => { + const out = execSync( + "npx wp-env run cli --env-cwd='wp-content/plugins/simple-events' -- wp eval-file tests/e2e/fixtures/seed-parity.php", + { encoding: 'utf8' } + ); + const m = out.match( /(\d+)\s*$/m ); + if ( ! m ) { + throw new Error( 'Seeder returned no page ID. Output:\n' + out ); + } + pageId = m[ 1 ]; + } ); + + /** Ordered, de-duplicated sequence of PARITY event numbers from a + * container's text. Array (not Set) preserves render order; first-seen + * dedupe collapses the nested block-wrapper text repeats. */ + function sequenceFrom( text ) { + const re = /PARITY (\d{2})/g; + const seen = new Set(); + const seq = []; + let m; + while ( ( m = re.exec( text || '' ) ) ) { + if ( ! seen.has( m[ 1 ] ) ) { + seen.add( m[ 1 ] ); + seq.push( m[ 1 ] ); + } + } + return seq; + } + + async function frontSequence( page ) { + await page.goto( `${ BASE_URL }/?page_id=${ pageId }` ); + const txt = await page + .locator( '.wp-block-query' ) + .first() + .innerText(); + return sequenceFrom( txt ); + } + + async function editorSequence( page ) { + await page.goto( `${ BASE_URL }/wp-admin/post.php?post=${ pageId }&action=edit` ); + await page.waitForFunction( () => + window.wp?.data + ?.select( 'core/block-editor' ) + ?.getBlocks() + ?.some( ( b ) => b.name === 'core/query' ) + ); + await page.evaluate( () => { + const prefs = window.wp.data.dispatch( 'core/preferences' ); + prefs && prefs.set && prefs.set( 'core/edit-post', 'welcomeGuide', false ); + } ); + // Wait for the query preview to resolve some PARITY items. + await expect + .poll( + async () => + sequenceFrom( + await page + .locator( '.wp-block-query' ) + .first() + .innerText() + .catch( () => '' ) + ).length, + { timeout: 15000 } + ) + .toBeGreaterThan( 0 ); + const txt = await page + .locator( '.wp-block-query' ) + .first() + .innerText(); + return sequenceFrom( txt ); + } + + test( 'editor preview matches the front-end (default feed)', async ( { page } ) => { + const front = await frontSequence( page ); + const editor = await editorSequence( page ); + + // Sanity: front-end page 1 has 6 events (perPage 6) from the 14 seeded. + expect( front ).toHaveLength( 6 ); + + // The real assertion: the editor preview's page 1 must be the SAME + // events in the SAME order as the front-end. RED if the editor query + // path selects/orders differently from build_query. + expect( editor ).toEqual( front ); + } ); +} ); diff --git a/tests/e2e/specs/frontend/loop-event-info.spec.js b/tests/e2e/specs/frontend/loop-event-info.spec.js new file mode 100644 index 0000000..09bf27e --- /dev/null +++ b/tests/e2e/specs/frontend/loop-event-info.spec.js @@ -0,0 +1,58 @@ +const { test, expect } = require( '@playwright/test' ); +const { execSync } = require( 'child_process' ); +const path = require( 'path' ); + +const BASE_URL = process.env.WP_BASE_URL || 'http://localhost:8888'; + +/** + * Front-end coverage for the loop-event-info enhancements on + * feature/the-pocket-features: + * - tagName → wrapper element is

, not
+ * - dateFormat override → date renders as Y/m/d, not the site default + * - query offset → 3 events seeded, offset:1 ⇒ exactly 2 rendered + * + * Seeds via wp-env cli (PHP seeder), then asserts the rendered page DOM. + */ +test.describe( 'loop-event-info front-end render', () => { + let pageId; + + test.beforeAll( () => { + const out = execSync( + "npx wp-env run cli --env-cwd='wp-content/plugins/simple-events' -- wp eval-file tests/e2e/fixtures/seed-loop.php", + { encoding: 'utf8' } + ); + const m = out.match( /(\d+)\s*$/m ); + if ( ! m ) { + throw new Error( 'Seeder did not return a page ID. Output:\n' + out ); + } + pageId = m[ 1 ]; + } ); + + test( 'tagName, dateFormat override and query offset all apply', async ( { page } ) => { + await page.goto( `${ BASE_URL }/?page_id=${ pageId }` ); + + const blocks = page.locator( 'h2.wp-block-simple-events-loop-event-info' ); + + // Screenshot for the eyeball check. + await page.screenshot( { + path: path.join( __dirname, '../../../../test-results/loop-event-info.png' ), + fullPage: true, + } ); + + // offset:1 of 3 seeded ⇒ exactly 2 rendered. + await expect( blocks ).toHaveCount( 2 ); + + // tagName=h2 → every loop-event-info is an

(selector already + // asserts the tag; this confirms the wrapper class is present too). + const count = await blocks.count(); + expect( count ).toBe( 2 ); + + // dateFormat="Y/m/d" → each rendered date matches \d{4}/\d{2}/\d{2} + // and NOT the site default (e.g. "May 25, 2026"). + for ( let i = 0; i < count; i++ ) { + const text = ( await blocks.nth( i ).innerText() ).trim(); + expect( text, `block ${ i } date format` ).toMatch( /\d{4}\/\d{2}\/\d{2}/ ); + expect( text, `block ${ i } not site-default format` ).not.toMatch( /[A-Za-z]{3,}/ ); + } + } ); +} );