-
+
- $text ) { ?>
+ $text ) { ?>
">
parsely->get_options()[ $key ];
$input_name = Parsely::OPTIONS_KEY . "[$key]";
$button_text = __( 'Browse', 'wp-parsely' );
?>
-
+
@@ -831,17 +866,23 @@ public function print_media_single_image( array $args ): void {
*
* @since 3.2.0
*
- * @param array $args The arguments used in the output HTML elements.
+ * @param Setting_Arguments $args The arguments used in the output HTML elements.
*/
- public function print_track_post_types_table( array $args ): void {
+ public function print_track_post_types_table( $args ): void {
$option_key = esc_attr( $args['option_key'] );
+ $title = $args['title'] ?? '';
+ /**
+ * Variable.
+ *
+ * @var array
+ */
$post_types = get_post_types( array( 'public' => true ) );
$values = $this->get_tracking_values_for_display();
?>
-
+
-
+
@@ -892,7 +933,7 @@ public function print_track_post_types_table( array $args ): void {
*
* @since 3.2.0
*
- * @return array Key-value pairs with post type and their 'track as' value.
+ * @return array Key-value pairs with post type and their 'track as' value.
*/
public function get_tracking_values_for_display(): array {
$options = $this->parsely->get_options();
@@ -900,11 +941,10 @@ public function get_tracking_values_for_display(): array {
$result = array();
foreach ( $types as $type ) {
- $array_key = "track_{$type}_types";
- if ( array_key_exists( $array_key, $options ) ) {
- foreach ( $options[ $array_key ] as $post_type ) {
- $result[ $post_type ] = $type;
- }
+ $array_value = $options[ "track_{$type}_types" ];
+
+ foreach ( $array_value as $post_type ) {
+ $result[ $post_type ] = $type;
}
}
@@ -914,41 +954,44 @@ public function get_tracking_values_for_display(): array {
/**
* Validates the options provided by the user.
*
- * @param array $input Options from the settings page.
- * @return array List of validated input settings.
+ * @param Parsely_Options $input Options from the settings page.
+ *
+ * @return Parsely_Options List of validated input settings.
*/
- public function validate_options( array $input ): array {
+ public function validate_options( $input ) {
$options = $this->parsely->get_options();
- if ( empty( $input['apikey'] ) ) {
+ if ( '' === $input['apikey'] ) {
add_settings_error(
Parsely::OPTIONS_KEY,
'apikey',
__( 'Please specify the Site ID', 'wp-parsely' )
);
} else {
- $api_key = $this->sanitize_api_key( $input['apikey'] );
- if ( false === $this->validate_api_key( $api_key ) ) {
+ $site_id = $this->sanitize_site_id( $input['apikey'] );
+ if ( false === $this->validate_site_id( $site_id ) ) {
add_settings_error(
Parsely::OPTIONS_KEY,
'apikey',
__( 'Your Parse.ly Site ID looks incorrect, it should look like "example.com".', 'wp-parsely' )
);
} else {
- $input['apikey'] = $api_key;
+ $input['apikey'] = $site_id;
}
}
$input['api_secret'] = sanitize_text_field( $input['api_secret'] );
- if ( ! empty( $input['metadata_secret'] ) ) {
+ if ( '' !== $input['metadata_secret'] ) {
if ( strlen( $input['metadata_secret'] ) !== 10 ) {
add_settings_error(
Parsely::OPTIONS_KEY,
'metadata_secret',
__( 'Metadata secret is incorrect. Please contact Parse.ly support!', 'wp-parsely' )
);
- } elseif ( isset( $input['parsely_wipe_metadata_cache'] ) && 'true' === $input['parsely_wipe_metadata_cache'] ) {
+ } elseif (
+ isset( $input['parsely_wipe_metadata_cache'] ) && 'true' === $input['parsely_wipe_metadata_cache'] // @phpstan-ignore-line
+ ) {
delete_post_meta_by_key( 'parsely_metadata_last_updated' );
wp_schedule_event( time() + 100, 'everytenminutes', 'parsely_bulk_metas_update' );
@@ -956,7 +999,7 @@ public function validate_options( array $input ): array {
}
}
- if ( empty( $input['logo'] ) ) {
+ if ( '' === $input['logo'] ) {
$input['logo'] = self::get_logo_default();
}
@@ -1121,7 +1164,7 @@ public function validate_options( array $input ): array {
}
/**
- * Validates the passed API key.
+ * Validates the passed Site ID.
*
* Accepts a www prefix and up to 3 periods.
*
@@ -1133,25 +1176,25 @@ public function validate_options( array $input ): array {
*
* @since 3.3.0
*
- * @param string $api_key The API key to be validated.
+ * @param string $site_id The Site ID to be validated.
* @return bool
*/
- private function validate_api_key( string $api_key ): bool {
+ private function validate_site_id( string $site_id ): bool {
$key_format = '/^((\w+)\.)?(([\w-]+)?)(\.[\w-]+){1,2}$/';
- return 1 === preg_match( $key_format, $api_key );
+ return 1 === preg_match( $key_format, $site_id );
}
/**
- * Sanitizes the passed API key.
+ * Sanitizes the passed Site ID.
*
* @since 3.3.0
*
- * @param string $api_key The API key to be sanitized.
+ * @param string $site_id The Site ID to be sanitized.
* @return string
*/
- private function sanitize_api_key( string $api_key ): string {
- return strtolower( sanitize_text_field( $api_key ) );
+ private function sanitize_site_id( string $site_id ): string {
+ return strtolower( sanitize_text_field( $site_id ) );
}
/**
@@ -1162,9 +1205,9 @@ private function sanitize_api_key( string $api_key ): string {
*
* @since 3.2.0
*
- * @param array $input Array passed to validate_options() function.
+ * @param Parsely_Options $input Array passed to validate_options() function.
*/
- private function validate_options_post_type_tracking( array &$input ): void {
+ private function validate_options_post_type_tracking( &$input ): void {
$options = $this->parsely->get_options();
$posts = 'track_post_types';
$pages = 'track_page_types';
@@ -1206,10 +1249,15 @@ private function validate_options_post_type_tracking( array &$input ): void {
* @return string
*/
private static function get_logo_default(): string {
+ /**
+ * Variable.
+ *
+ * @var int
+ */
$custom_logo_id = get_theme_mod( 'custom_logo' );
- if ( $custom_logo_id ) {
+ if ( (bool) $custom_logo_id ) {
$logo_attrs = wp_get_attachment_image_src( $custom_logo_id, 'full' );
- if ( $logo_attrs ) {
+ if ( isset( $logo_attrs[0] ) ) {
return $logo_attrs[0];
}
}
@@ -1222,8 +1270,8 @@ private static function get_logo_default(): string {
/**
* Sanitizes all elements in an option array.
*
- * @param array $array Array of options to be sanitized.
- * @return array
+ * @param array $array Array of options to be sanitized.
+ * @return array
*/
private static function sanitize_option_array( array $array ): array {
$new_array = $array;
diff --git a/src/UI/class-site-health.php b/src/UI/class-site-health.php
index d3364c6eb..b7c144b87 100644
--- a/src/UI/class-site-health.php
+++ b/src/UI/class-site-health.php
@@ -18,6 +18,17 @@
* Provides debug information about the plugin.
*
* @since 3.4.0
+ *
+ * @phpstan-type Site_Health_Info array{
+ * parsely?: Parsely_Health_Info
+ * }
+ *
+ * @phpstan-type Parsely_Health_Info array{
+ * label: string,
+ * description: string,
+ * show_count: bool,
+ * fields: array,
+ * }
*/
final class Site_Health {
/**
@@ -42,7 +53,7 @@ public function __construct( Parsely $parsely ) {
* @since 3.4.0
*/
public function run(): void {
- add_filter( 'site_status_tests', array( $this, 'check_api_key' ) );
+ add_filter( 'site_status_tests', array( $this, 'check_site_id' ) );
add_filter( 'debug_information', array( $this, 'options_debug_info' ) );
}
@@ -53,9 +64,9 @@ public function run(): void {
*
* @param array $tests An associative array of direct and asynchronous tests.
*
- * @return array
+ * @return array
*/
- public function check_api_key( array $tests ): array {
+ public function check_site_id( array $tests ): array {
$test = function() {
$result = array(
'label' => __( 'The Site ID is correctly set up', 'wp-parsely' ),
@@ -71,7 +82,7 @@ public function check_api_key( array $tests ): array {
'test' => 'loopback_requests',
);
- if ( $this->parsely->api_key_is_missing() ) {
+ if ( $this->parsely->site_id_is_missing() ) {
$result['status'] = 'critical';
$result['label'] = __( 'You need to provide the Site ID', 'wp-parsely' );
$result['actions'] = __( 'The site ID can be set in the Parse.ly Settings Page .', 'wp-parsely' );
@@ -80,7 +91,13 @@ public function check_api_key( array $tests ): array {
return $result;
};
- $tests['direct']['parsely'] = array(
+ /**
+ * Variable.
+ *
+ * @var array
+ */
+ $direct = $tests['direct'];
+ $direct['parsely'] = array(
'label' => __( 'Parse.ly Site ID', 'wp-parsely' ),
'test' => $test,
);
@@ -93,11 +110,11 @@ public function check_api_key( array $tests ): array {
*
* @since 3.4.0
*
- * @param array $args The debug information to be added to the core information page.
+ * @param Site_Health_Info $args The debug information to be added to the core information page.
*
- * @return array
+ * @return Site_Health_Info
*/
- public function options_debug_info( array $args ): array {
+ public function options_debug_info( $args ) {
$options = $this->parsely->get_options();
$args['parsely'] = array(
diff --git a/src/Utils/utils.php b/src/Utils/utils.php
new file mode 100644
index 000000000..4c0bc90bf
--- /dev/null
+++ b/src/Utils/utils.php
@@ -0,0 +1,247 @@
+format( $number );
+
+ if ( false === $formatted_number ) {
+ return '';
+ }
+
+ return $formatted_number;
+}
+
+/**
+ * Gets time in formatted form.
+ *
+ * Example:
+ * - Input `1000` (seconds) and Output `16:40` which represents "16 minutes, 40 seconds”
+ *
+ * @since 3.7.0
+ *
+ * @param int|float $seconds Time in seconds that we have to format.
+ *
+ * @return string
+ */
+function get_formatted_time( $seconds ): string {
+ $time_formatter = new NumberFormatter( 'en_US', NumberFormatter::DURATION );
+ $formatted_time = $time_formatter->format( $seconds );
+
+ if ( false === $formatted_time ) {
+ return '';
+ }
+
+ return $formatted_time;
+}
+
+/**
+ * Converts to associate array.
+ *
+ * @since 3.7.0
+ *
+ * @param mixed $obj Input object.
+ *
+ * @return array|WP_Error
+ */
+function convert_to_associative_array( $obj ) {
+ $encoded = wp_json_encode( $obj );
+
+ if ( false === $encoded ) {
+ return new WP_Error( 'parsely_encoding_failed', __( 'Unable to encode API response for associative array', 'wp-parsely' ) );
+ }
+
+ /**
+ * Variable.
+ *
+ * @var array
+ */
+ return json_decode( $encoded, true );
+}
+
+/**
+ * Converts a string to a positive integer, removing any non-numeric
+ * characters.
+ *
+ * @param string $string The string to be converted to an integer.
+ * @return int The integer resulting from the conversion.
+ */
+function convert_to_positive_integer( string $string ): int {
+ return (int) preg_replace( '/\D/', '', $string );
+}
+
+/**
+ * Converts endpoint to filter key by replacing `/` with `_`.
+ *
+ * @param string $endpoint Route of the endpoint.
+ *
+ * @since 3.7.0
+ *
+ * @return string
+ */
+function convert_endpoint_to_filter_key( string $endpoint ): string {
+ return trim( str_replace( '/', '_', $endpoint ), '_' );
+}
diff --git a/src/blocks/content-helper/class-content-helper.php b/src/blocks/content-helper/class-content-helper.php
index 24c5afae2..e5f10d97c 100644
--- a/src/blocks/content-helper/class-content-helper.php
+++ b/src/blocks/content-helper/class-content-helper.php
@@ -1,6 +1,6 @@
span {
- color: $gray-600;
- margin-bottom: 0;
- display: flex;
+ .parsely-top-post-view-link,
+ .parsely-top-post-edit-link {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ position: relative;
+ margin-right: to_rem(3px);
+
+ svg {
+ position: absolute;
+ top: 2px;
+ fill: #8d98a1;
+ }
- &:not(:first-child) {
- margin-left: to_rem(5px);
+ &:hover svg {
+ fill: var(--blue-550);
}
}
-}
-.parsely-top-post-views svg {
- position: relative;
- top: 2px;
- margin-right: to_rem(3px);
- fill: $gray-600;
-}
+ .parsely-top-post-info {
+ display: flex;
+ margin: to_rem(5px) 0 0;
+ justify-content: space-between;
+ align-items: center;
-.parsely-top-post-link:hover svg {
- fill: $blue-550;
-}
+ >span {
+ color: var(--gray-600);
+ margin-bottom: 0;
+ display: flex;
-.parsely-contact-us {
- margin-top: to_rem(15px) !important;
+ &:not(:first-child) {
+ margin-left: to_rem(5px);
+ }
+ }
+ }
}
-div.wp-parsely-content-helper {
+.wp-parsely-content-helper .performance-details-panel {
- div.current-post-details-panel {
+ // Generic styles for all sections.
+ div.section {
+ font-family: var(--base-font);
+ margin-top: 1.8rem;
- // Generic styles for all sections.
- div.section {
- font-family: var(--base-font);
- margin-top: 1.8rem;
-
- table {
- border-collapse: collapse;
- width: 100%;
+ table {
+ border-collapse: collapse;
+ width: 100%;
- th {
- font-weight: 400;
- text-align: left;
- }
+ th {
+ font-weight: 400;
+ text-align: left;
}
+ }
- // Generic styles for section titles.
- div.section-title {
- color: var(--base-text-2);
- margin-bottom: 0.5rem;
- }
+ // Generic styles for section titles.
+ div.section-title {
+ color: var(--base-text-2);
+ margin-bottom: 0.5rem;
}
+ }
- // Data Period section (Last x days).
- div.section.period {
- margin-top: 0.8rem;
+ // Data Period section (Last x days).
+ div.section.period {
+ margin-top: 0.8rem;
- span {
- color: var(--base-text-2);
- }
+ span {
+ color: var(--base-text-2);
}
+ }
- // General Performance section (Views, Visitors, Time).
- div.section.general-performance {
+ // General Performance section (Views, Visitors, Time).
+ div.section.general-performance {
- table {
- // Metrics.
- tbody tr {
- font-family: var(--numeric-font);
- font-size: var(--font-size--extra-large);
- font-weight: 500;
- }
+ table {
- // Titles.
- tfoot tr {
- color: var(--gray-700);
- height: 1.4rem;
- vertical-align: bottom;
- }
+ // Metrics.
+ tbody tr {
+ font-family: var(--numeric-font);
+ font-size: var(--font-size--extra-large);
+ font-weight: 500;
+ }
+
+ // Titles.
+ tfoot tr {
+ color: var(--gray-700);
+ height: 1.4rem;
+ vertical-align: bottom;
}
}
+ }
- // Referrer Types section.
- div.section.referrer-types {
+ // Referrer Types section.
+ div.section.referrer-types {
- // Multi-percentage bar.
- div.multi-percentage-bar {
- --radius: 2px;
- display: flex;
- height: 0.5rem;
+ // Multi-percentage bar.
+ div.multi-percentage-bar {
+ --radius: 2px;
+ display: flex;
+ height: 0.5rem;
- .bar-fill {
+ .bar-fill {
- // Border radiuses for first and last bar-fills.
- &:first-child {
- border-radius: var(--radius) 0 0 var(--radius);
- }
+ // Border radiuses for first and last bar-fills.
+ &:first-child {
+ border-radius: var(--radius) 0 0 var(--radius);
+ }
- &:last-child {
- border-radius: 0 var(--radius) var(--radius) 0;
- }
+ &:last-child {
+ border-radius: 0 var(--radius) var(--radius) 0;
+ }
- // Bar-fill colors by referrer type.
- &.direct {
- background-color: hsl(var(--ref-direct));
- }
+ // Bar-fill colors by referrer type.
+ &.direct {
+ background-color: hsl(var(--ref-direct));
+ }
- &.internal {
- background-color: hsl(var(--ref-internal));
- }
+ &.internal {
+ background-color: hsl(var(--ref-internal));
+ }
- &.search {
- background-color: hsl(var(--ref-search));
- }
+ &.search {
+ background-color: hsl(var(--ref-search));
+ }
- &.social {
- background-color: hsl(var(--ref-social));
- }
+ &.social {
+ background-color: hsl(var(--ref-social));
+ }
- &.other {
- background-color: hsl(var(--ref-other));
- }
+ &.other {
+ background-color: hsl(var(--ref-other));
}
}
+ }
- // Table showing referrer types and metrics.
- table {
- margin-top: 0.5rem;
+ // Table showing referrer types and metrics.
+ table {
+ margin-top: 0.5rem;
- // Metrics.
- tbody tr {
- font-family: var(--numeric-font);
- font-size: var(--font-size--large);
- height: 1.4rem;
- vertical-align: bottom;
- }
+ // Metrics.
+ tbody tr {
+ font-family: var(--numeric-font);
+ font-size: var(--font-size--large);
+ height: 1.4rem;
+ vertical-align: bottom;
}
}
+ }
+
+ // Top Referrers section.
+ div.section.top-referrers {
- // Top Referrers section.
- div.section.top-referrers {
+ table {
- table {
+ // Titles (Top Referrers, Page Views).
+ thead tr {
+ color: var(--base-text-2);
+ height: 1.6rem;
+ vertical-align: top;
- // Titles (Top Referrers, Page Views).
- thead tr {
- color: var(--base-text-2);
- height: 1.6rem;
- vertical-align: top;
+ th:last-child {
+ text-align: right;
+ }
+ }
- th:last-child {
- text-align: right;
+ // Table rows.
+ tbody {
+
+ tr {
+ border: 1px solid var(--border);
+ border-left: 0;
+ border-right: 0;
+ height: 2rem;
+
+ // Referrer name column.
+ th:first-child {
+ --width: 8rem;
+ // Use min and max width for text truncation to work.
+ max-width: var(--width);
+ min-width: var(--width);
+ overflow: hidden;
+ padding-right: 1rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
- }
- // Table rows.
- tbody {
-
- tr {
- border: 1px solid var(--border);
- border-left: 0;
- border-right: 0;
- height: 2rem;
-
- // Referrer name column.
- th:first-child {
- --width: 8rem;
- // Use min and max width for text truncation to work.
- max-width: var(--width);
- min-width: var(--width);
- overflow: hidden;
- padding-right: 1rem;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- // Percentage bar column.
- td:nth-child(2) {
- width: 100%;
- }
-
- // Views column.
- td:last-child {
- padding-left: 1rem;
- text-align: right;
- }
+ // Percentage bar column.
+ td:nth-child(2) {
+ width: 100%;
}
- // Percentage bar.
- div.percentage-bar {
- // Bar background.
- --radius: 4px;
- background-color: var(--base-3);
- border-radius: var(--radius);
- display: flex;
- height: 0.4rem;
- margin: 0;
- overflow: hidden;
+ // Views column.
+ td:last-child {
+ padding-left: 1rem;
+ text-align: right;
+ }
+ }
- // Bar fill.
- &::after {
- background-color: var(--data);
- border-radius: var(--radius);
- content: "";
- height: 100%;
- width: var(--bar-fill);
- }
+ // Percentage bar.
+ div.percentage-bar {
+ // Bar background.
+ --radius: 4px;
+ background-color: var(--base-3);
+ border-radius: var(--radius);
+ display: flex;
+ height: 0.4rem;
+ margin: 0;
+ overflow: hidden;
+
+ // Bar fill.
+ &::after {
+ background-color: var(--data);
+ border-radius: var(--radius);
+ content: "";
+ height: 100%;
+ width: var(--bar-fill);
}
}
}
+ }
- // Percentage text below table.
- div:last-child {
- color: var(--base-text-2);
- margin-top: 0.6rem;
- }
+ // Percentage text below table.
+ div:last-child {
+ color: var(--base-text-2);
+ margin-top: 0.6rem;
}
+ }
- // Actions section (Visit Post, View in Parse.ly buttons).
- div.section.actions {
- display: inline-flex;
- justify-content: space-between;
- width: 100%;
+ // Actions section (Visit Post, View in Parse.ly buttons).
+ div.section.actions {
+ display: inline-flex;
+ justify-content: space-between;
+ width: 100%;
- a.components-button {
- border-radius: 4px;
- text-transform: uppercase;
+ a.components-button {
+ border-radius: 4px;
+ text-transform: uppercase;
- // Visit Post.
- &.is-secondary {
- box-shadow: inset 0 0 0 1px var(--border);
- color: var(--sidebar-black);
- }
+ // Visit Post.
+ &.is-secondary {
+ box-shadow: inset 0 0 0 1px var(--border);
+ color: var(--sidebar-black);
+ }
- // View in Parse.ly.
- &.is-primary {
- background-color: var(--control);
- }
+ // View in Parse.ly.
+ &.is-primary {
+ background-color: var(--control);
}
}
}
diff --git a/src/blocks/content-helper/content-helper.tsx b/src/blocks/content-helper/content-helper.tsx
index 6ee8169fc..2cd15751b 100644
--- a/src/blocks/content-helper/content-helper.tsx
+++ b/src/blocks/content-helper/content-helper.tsx
@@ -9,21 +9,21 @@ import { registerPlugin } from '@wordpress/plugins';
/**
* Internal dependencies
*/
-import CurrentPostDetails from './current-post-details/component';
-import RelatedTopPostList from './components/related-top-post-list';
+import PerformanceDetails from './performance-details/component';
+import RelatedTopPostList from './related-top-posts/component-list';
import LeafIcon from '../shared/components/leaf-icon';
const BLOCK_PLUGIN_ID = 'wp-parsely-block-editor-sidebar';
const renderSidebar = () => (
- } name="wp-parsely-content-helper" className="wp-parsely-content-helper" title={ __( 'Parse.ly Content Helper', 'wp-parsely' ) }>
+ } name="wp-parsely-content-helper" className="wp-parsely-content-helper" title={ __( 'Parse.ly Editor Sidebar', 'wp-parsely' ) }>
-
+
-
+
diff --git a/src/blocks/content-helper/icons/edit-icon.tsx b/src/blocks/content-helper/icons/edit-icon.tsx
new file mode 100644
index 000000000..9349df00b
--- /dev/null
+++ b/src/blocks/content-helper/icons/edit-icon.tsx
@@ -0,0 +1,19 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/components';
+
+export const EditIcon = () => (
+
+
+
+);
+
+export default EditIcon;
diff --git a/src/blocks/content-helper/icons/published-link-icon.tsx b/src/blocks/content-helper/icons/open-link-icon.tsx
similarity index 94%
rename from src/blocks/content-helper/icons/published-link-icon.tsx
rename to src/blocks/content-helper/icons/open-link-icon.tsx
index 9679b4605..c3d8c5823 100644
--- a/src/blocks/content-helper/icons/published-link-icon.tsx
+++ b/src/blocks/content-helper/icons/open-link-icon.tsx
@@ -3,7 +3,7 @@
*/
import { SVG, Path } from '@wordpress/components';
-export const ViewsIcon = () => (
+export const OpenLinkIcon = () => (
(
);
-export default ViewsIcon;
+export default OpenLinkIcon;
diff --git a/src/blocks/content-helper/current-post-details/component.tsx b/src/blocks/content-helper/performance-details/component.tsx
similarity index 65%
rename from src/blocks/content-helper/current-post-details/component.tsx
rename to src/blocks/content-helper/performance-details/component.tsx
index 3f4a1cb2b..10a2aa10d 100644
--- a/src/blocks/content-helper/current-post-details/component.tsx
+++ b/src/blocks/content-helper/performance-details/component.tsx
@@ -8,9 +8,10 @@ import { useEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
-import CurrentPostDetailsProvider from './provider';
-import { PostPerformanceData } from './post-performance-data';
+import PerformanceDetailsProvider from './provider';
+import { PerformanceData } from './model';
import { ContentHelperError } from '../content-helper-error';
+import { formatToImpreciseNumber } from '../../shared/functions';
// Number of attempts to fetch the data before displaying an error.
const FETCH_RETRIES = 3;
@@ -18,22 +19,22 @@ const FETCH_RETRIES = 3;
/**
* Specifies the form of component props.
*/
-interface PostDetailsSectionProps {
- data: PostPerformanceData;
+interface PerformanceSectionProps {
+ data: PerformanceData;
}
/**
* Outputs the current post's details or shows an error message on failure.
*/
-function CurrentPostDetails() {
+function PerformanceDetails() {
const [ loading, setLoading ] = useState( true );
- const [ error, setError ] = useState( null );
- const [ postDetailsData, setPostDetails ] = useState( null );
- const provider = new CurrentPostDetailsProvider();
+ const [ error, setError ] = useState();
+ const [ postDetailsData, setPostDetails ] = useState();
+ const provider = new PerformanceDetailsProvider();
useEffect( () => {
const fetchPosts = async ( retries: number ) => {
- provider.getCurrentPostDetails()
+ provider.getPerformanceDetails()
.then( ( result ) => {
setPostDetails( result );
setLoading( false );
@@ -59,19 +60,25 @@ function CurrentPostDetails() {
return (
loading
- ?
- :
+ ? (
+
+
+
+ )
+ : (
+
+ )
);
}
/**
* Outputs all the "Current Post Details" sections.
*
- * @param {PostDetailsSectionProps} props The props needed to populate the sections.
+ * @param {PerformanceSectionProps} props The props needed to populate the sections.
*/
-function CurrentPostDetailsSections( props: PostDetailsSectionProps ) {
+function PerformanceDetailsSections( props: PerformanceSectionProps ) {
return (
-
+
@@ -85,9 +92,9 @@ function CurrentPostDetailsSections( props: PostDetailsSectionProps ) {
* Outputs the "Period" section, which denotes the period for which data is
* shown.
*
- * @param {PostDetailsSectionProps} props The props needed to populate the section.
+ * @param {PerformanceSectionProps} props The props needed to populate the section.
*/
-function DataPeriodSection( props: PostDetailsSectionProps ) {
+function DataPeriodSection( props: PerformanceSectionProps ) {
const period = props.data.period;
// Get the date (in short format) on which the period starts.
@@ -115,9 +122,9 @@ function DataPeriodSection( props: PostDetailsSectionProps ) {
/**
* Outputs the "General Performance" (Views, Visitors, Time) section.
*
- * @param {PostDetailsSectionProps} props The props needed to populate the section.
+ * @param {PerformanceSectionProps} props The props needed to populate the section.
*/
-function GeneralPerformanceSection( props: PostDetailsSectionProps ) {
+function GeneralPerformanceSection( props: PerformanceSectionProps ) {
const data = props.data;
return (
@@ -125,8 +132,8 @@ function GeneralPerformanceSection( props: PostDetailsSectionProps ) {
- { impreciseNumber( data.views ) }
- { impreciseNumber( data.visitors ) }
+ { formatToImpreciseNumber( data.views ) }
+ { formatToImpreciseNumber( data.visitors ) }
{ data.avgEngaged }
@@ -145,9 +152,9 @@ function GeneralPerformanceSection( props: PostDetailsSectionProps ) {
/**
* Outputs the "Referrer Types" section.
*
- * @param {PostDetailsSectionProps} props The props needed to populate the section.
+ * @param {PerformanceSectionProps} props The props needed to populate the section.
*/
-function ReferrerTypesSection( props: PostDetailsSectionProps ) {
+function ReferrerTypesSection( props: PerformanceSectionProps ) {
const data = props.data;
// Remove unneeded totals to simplify upcoming map() calls.
@@ -198,7 +205,7 @@ function ReferrerTypesSection( props: PostDetailsSectionProps ) {
{
Object.entries( data.referrers.types ).map( ( [ key, value ] ) => {
- return { impreciseNumber( value.views ) } ;
+ return { formatToImpreciseNumber( value.views ) } ;
} ) }
@@ -210,9 +217,9 @@ function ReferrerTypesSection( props: PostDetailsSectionProps ) {
/**
* Outputs the "Top Referrers" section.
*
- * @param {PostDetailsSectionProps} props The props needed to populate the section.
+ * @param {PerformanceSectionProps} props The props needed to populate the section.
*/
-function TopReferrersSection( props: PostDetailsSectionProps ) {
+function TopReferrersSection( props: PerformanceSectionProps ) {
const data = props.data;
let totalViewsPercentage = 0;
@@ -249,7 +256,7 @@ function TopReferrersSection( props: PostDetailsSectionProps ) {
style={ { '--bar-fill': value.viewsPercentage + '%' } as React.CSSProperties }>
- { impreciseNumber( value.views ) }
+ { formatToImpreciseNumber( value.views ) }
);
} )
@@ -272,9 +279,9 @@ function TopReferrersSection( props: PostDetailsSectionProps ) {
/**
* Outputs the "Actions" section.
*
- * @param {PostDetailsSectionProps} props The props needed to populate the section.
+ * @param {PerformanceSectionProps} props The props needed to populate the section.
*/
-function ActionsSection( props: PostDetailsSectionProps ) {
+function ActionsSection( props: PerformanceSectionProps ) {
const data = props.data;
const ariaOpensNewTab = {
__( '(opens in new tab)', 'wp-parsely' ) }
@@ -287,67 +294,11 @@ function ActionsSection( props: PostDetailsSectionProps ) {
{ __( 'Visit Post', 'wp-parsely' ) }{ ariaOpensNewTab }
+ href={ data.dashUrl } rel="noopener" target="_blank" variant="primary">
{ __( 'View in Parse.ly', 'wp-parsely' ) }{ ariaOpensNewTab }
);
}
-/**
- * Implements the "Imprecise Number" functionality of the Parse.ly dashboard.
- *
- * Note: This function is not made to process float numbers.
- *
- * @param {string} value The number to process. It can be formatted.
- * @param {number} fractionDigits The number of desired fraction digits.
- * @param {string} glue A string to put between the number and unit.
- * @return {string} The number formatted as an imprecise number.
- */
-function impreciseNumber( value: string, fractionDigits = 1, glue = '' ): string {
- const number = parseInt( value.replace( /\D/g, '' ), 10 );
-
- if ( number < 1000 ) {
- return value;
- } else if ( number < 10000 ) {
- fractionDigits = 1;
- }
-
- const unitNames = {
- 1000: 'k',
- '1,000,000': 'M',
- '1,000,000,000': 'B',
- '1,000,000,000,000': 'T',
- '1,000,000,000,000,000': 'Q',
- };
- let currentNumber = number;
- let currentNumberAsString = number.toString();
- let unit = '';
- let previousNumber = 0;
-
- Object.entries( unitNames ).forEach( ( [ thousands, suffix ] ) => {
- const thousandsInt = parseInt( thousands.replace( /\D/g, '' ), 10 );
-
- if ( number >= thousandsInt ) {
- currentNumber = number / thousandsInt;
- let precision = fractionDigits;
-
- // For over 10 units, we reduce the precision to 1 fraction digit.
- if ( currentNumber % 1 > 1 / previousNumber ) {
- precision = currentNumber > 10 ? 1 : 2;
- }
-
- // Precision override, where we want to show 2 fraction digits.
- const zeroes = parseFloat( currentNumber.toFixed( 2 ) ) === parseFloat( currentNumber.toFixed( 0 ) );
- precision = zeroes ? 0 : precision;
- currentNumberAsString = currentNumber.toFixed( precision );
- unit = suffix;
- }
-
- previousNumber = thousandsInt;
- } );
-
- return currentNumberAsString + glue + unit;
-}
-
-export default CurrentPostDetails;
+export default PerformanceDetails;
diff --git a/src/blocks/content-helper/current-post-details/post-performance-data.ts b/src/blocks/content-helper/performance-details/model.ts
similarity index 70%
rename from src/blocks/content-helper/current-post-details/post-performance-data.ts
rename to src/blocks/content-helper/performance-details/model.ts
index 80974e704..5dcd26adb 100644
--- a/src/blocks/content-helper/current-post-details/post-performance-data.ts
+++ b/src/blocks/content-helper/performance-details/model.ts
@@ -1,4 +1,4 @@
-export interface PostPerformanceData {
+export interface PerformanceData {
author: string;
avgEngaged: string;
date: string;
@@ -8,15 +8,15 @@ export interface PostPerformanceData {
end: string;
days: number;
};
- referrers: PostPerformanceReferrerData;
- statsUrl: string;
+ referrers: PerformanceReferrerData;
+ dashUrl: string;
title: string;
url: string;
views: string;
visitors: string;
}
-export interface PostPerformanceReferrerData {
+export interface PerformanceReferrerData {
top: {
views: string;
viewsPercentage: string;
diff --git a/src/blocks/content-helper/current-post-details/provider.ts b/src/blocks/content-helper/performance-details/provider.ts
similarity index 68%
rename from src/blocks/content-helper/current-post-details/provider.ts
rename to src/blocks/content-helper/performance-details/provider.ts
index d60132d6f..cdadc59a9 100644
--- a/src/blocks/content-helper/current-post-details/provider.ts
+++ b/src/blocks/content-helper/performance-details/provider.ts
@@ -14,9 +14,13 @@ import {
ContentHelperErrorCode,
} from '../content-helper-error';
import {
- PostPerformanceData,
- PostPerformanceReferrerData,
-} from './post-performance-data';
+ PerformanceData,
+ PerformanceReferrerData,
+} from './model';
+import {
+ convertDateToString,
+ removeDaysFromDate,
+} from '../../shared/utils/date';
/**
* Specifies the form of the response returned by the `/stats/post/detail`
@@ -24,7 +28,7 @@ import {
*/
interface AnalyticsApiResponse {
error?: Error;
- data: PostPerformanceData[];
+ data: PerformanceData[];
}
/**
@@ -33,13 +37,13 @@ import {
*/
interface ReferrersApiResponse {
error?: Error;
- data: PostPerformanceReferrerData;
+ data: PerformanceReferrerData;
}
/**
* Provides current post details data for use in other components.
*/
-class CurrentPostDetailsProvider {
+class PerformanceDetailsProvider {
private dataPeriodDays: number;
private dataPeriodStart: string;
private dataPeriodEnd: string;
@@ -48,17 +52,19 @@ class CurrentPostDetailsProvider {
* Constructor.
*/
constructor() {
- // Return data for the last 7 days (today included).
- this.setDataPeriod( 7 );
+ // Set period for the last 7 days (today included).
+ this.dataPeriodDays = 7;
+ this.dataPeriodEnd = convertDateToString( new Date() ) + 'T23:59';
+ this.dataPeriodStart = removeDaysFromDate( this.dataPeriodEnd, this.dataPeriodDays - 1 ) + 'T00:00';
}
/**
* Returns details about the post that is currently being edited within the
* WordPress Block Editor.
*
- * @return {Promise} The current post's details.
+ * @return {Promise} The current post's details.
*/
- public async getCurrentPostDetails(): Promise {
+ public async getPerformanceDetails(): Promise {
const editor = select( 'core/editor' );
// We cannot show data for non-published posts.
@@ -92,9 +98,9 @@ class CurrentPostDetailsProvider {
* API.
*
* @param {string} postUrl
- * @return {Promise } The current post's details.
+ * @return {Promise } The current post's details.
*/
- private async fetchPerformanceDataFromWpEndpoint( postUrl: string ): Promise {
+ private async fetchPerformanceDataFromWpEndpoint( postUrl: string ): Promise {
let response;
try {
@@ -106,7 +112,7 @@ class CurrentPostDetailsProvider {
period_end: this.dataPeriodEnd,
} ),
} );
- } catch ( wpError ) {
+ } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any
return Promise.reject( new ContentHelperError(
wpError.message, wpError.code
) );
@@ -124,7 +130,7 @@ class CurrentPostDetailsProvider {
return Promise.reject( new ContentHelperError(
sprintf(
/* translators: URL of the published post */
- __( 'The post %s has 0 views or no data was returned for it by the Parse.ly API.',
+ __( 'The post %s has 0 views, or the Parse.ly API returned no data.',
'wp-parsely' ), postUrl
), ContentHelperErrorCode.ParselyApiReturnedNoData, ''
) );
@@ -149,11 +155,11 @@ class CurrentPostDetailsProvider {
*
* @param {string} postUrl The post's URL.
* @param {string} totalViews Total post views (including direct views).
- * @return {Promise} The post's referrer data.
+ * @return {Promise} The post's referrer data.
*/
private async fetchReferrerDataFromWpEndpoint(
postUrl: string, totalViews: string
- ): Promise {
+ ): Promise {
let response;
// Query WordPress API endpoint.
@@ -166,7 +172,7 @@ class CurrentPostDetailsProvider {
total_views: totalViews, // Needed to calculate direct views.
} ),
} );
- } catch ( wpError ) {
+ } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any
return Promise.reject( new ContentHelperError(
wpError.message, wpError.code
) );
@@ -181,42 +187,6 @@ class CurrentPostDetailsProvider {
return response.data;
}
-
- /**
- * Sets the period for which to fetch the data.
- *
- * @param {number} days Number of last days to get the data for.
- */
- private setDataPeriod( days: number ) {
- this.dataPeriodDays = days;
- this.dataPeriodEnd = this.convertDateToString( new Date() ) + 'T23:59';
- this.dataPeriodStart = this.removeDaysFromDate( this.dataPeriodEnd, this.dataPeriodDays - 1 ) + 'T00:00';
- }
-
- /**
- * Removes the given number of days from a "YYYY-MM-DD" string, and returns
- * the result in the same format.
- *
- * @param {string} date The date in "YYYY-MM-DD" format.
- * @param {number} days The number of days to remove from the date.
- * @return {string} The resulting date in "YYYY-MM-DD" format.
- */
- private removeDaysFromDate( date: string, days: number ): string {
- const pastDate = new Date( date );
- pastDate.setDate( pastDate.getDate() - days );
-
- return this.convertDateToString( pastDate );
- }
-
- /**
- * Converts a date to a string in "YYYY-MM-DD" format.
- *
- * @param {Date} date The date to format.
- * @return {string} The date in "YYYY-MM-DD" format.
- */
- private convertDateToString( date: Date ): string {
- return date.toISOString().substring( 0, 10 );
- }
}
-export default CurrentPostDetailsProvider;
+export default PerformanceDetailsProvider;
diff --git a/src/blocks/content-helper/components/related-top-post-list-item.tsx b/src/blocks/content-helper/related-top-posts/component-list-item.tsx
similarity index 54%
rename from src/blocks/content-helper/components/related-top-post-list-item.tsx
rename to src/blocks/content-helper/related-top-posts/component-list-item.tsx
index d9b930623..f81c249d4 100644
--- a/src/blocks/content-helper/components/related-top-post-list-item.tsx
+++ b/src/blocks/content-helper/related-top-posts/component-list-item.tsx
@@ -6,9 +6,11 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
-import { RelatedTopPostData } from '../models/related-top-post-data';
+import { RelatedTopPostData } from './model';
import ViewsIcon from '../icons/views-icon';
-import PublishedLinkIcon from '../icons/published-link-icon';
+import OpenLinkIcon from '../icons/open-link-icon';
+import EditIcon from '../icons/edit-icon';
+import { getPostEditUrl } from '../../shared/utils/post';
interface RelatedTopPostListItemProps {
post: RelatedTopPostData;
@@ -18,12 +20,20 @@ function RelatedTopPostListItem( { post }: RelatedTopPostListItemProps ): JSX.El
return (
Date { post.date }
diff --git a/src/blocks/content-helper/components/related-top-post-list.tsx b/src/blocks/content-helper/related-top-posts/component-list.tsx
similarity index 84%
rename from src/blocks/content-helper/components/related-top-post-list.tsx
rename to src/blocks/content-helper/related-top-posts/component-list.tsx
index 2bd126d26..5a7915049 100644
--- a/src/blocks/content-helper/components/related-top-post-list.tsx
+++ b/src/blocks/content-helper/related-top-posts/component-list.tsx
@@ -7,11 +7,11 @@ import { useEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
-import ContentHelperProvider from '../content-helper-provider';
-import RelatedTopPostListItem from './related-top-post-list-item';
+import RelatedTopPostsProvider from './provider';
+import RelatedTopPostListItem from './component-list-item';
+import { RelatedTopPostData } from './model';
import { ContentHelperError } from '../content-helper-error';
import { getDateInUserLang, SHORT_DATE_FORMAT } from '../../shared/utils/date';
-import { RelatedTopPostData } from '../models/related-top-post-data';
const FETCH_RETRIES = 3;
@@ -20,13 +20,13 @@ const FETCH_RETRIES = 3;
*/
function RelatedTopPostList() {
const [ loading, setLoading ] = useState( true );
- const [ error, setError ] = useState( null );
- const [ message, setMessage ] = useState( null );
+ const [ error, setError ] = useState();
+ const [ message, setMessage ] = useState();
const [ posts, setPosts ] = useState( [] );
useEffect( () => {
const fetchPosts = async ( retries: number ) => {
- ContentHelperProvider.getRelatedTopPosts()
+ RelatedTopPostsProvider.getRelatedTopPosts()
.then( ( result ): void => {
const mappedPosts: RelatedTopPostData[] = result.posts.map(
( post: RelatedTopPostData ): RelatedTopPostData => (
@@ -59,7 +59,7 @@ function RelatedTopPostList() {
setLoading( false );
setPosts( [] );
setMessage( '' );
- setError( null );
+ setError( undefined );
};
}, [] );
diff --git a/src/blocks/content-helper/models/related-top-post-data.ts b/src/blocks/content-helper/related-top-posts/model.ts
similarity index 78%
rename from src/blocks/content-helper/models/related-top-post-data.ts
rename to src/blocks/content-helper/related-top-posts/model.ts
index a68f978fb..4b1c41ee7 100644
--- a/src/blocks/content-helper/models/related-top-post-data.ts
+++ b/src/blocks/content-helper/related-top-posts/model.ts
@@ -2,7 +2,8 @@ export interface RelatedTopPostData {
author: string;
date: string;
id: number;
- statsUrl: string;
+ postId: number;
+ dashUrl: string;
title: string;
url: string;
views: number;
diff --git a/src/blocks/content-helper/content-helper-provider.ts b/src/blocks/content-helper/related-top-posts/provider.ts
similarity index 86%
rename from src/blocks/content-helper/content-helper-provider.ts
rename to src/blocks/content-helper/related-top-posts/provider.ts
index 4ccd99914..56c4e545c 100644
--- a/src/blocks/content-helper/content-helper-provider.ts
+++ b/src/blocks/content-helper/related-top-posts/provider.ts
@@ -14,8 +14,8 @@ import apiFetch from '@wordpress/api-fetch';
import {
ContentHelperError,
ContentHelperErrorCode,
-} from './content-helper-error';
-import { RelatedTopPostData } from './models/related-top-post-data';
+} from '../content-helper-error';
+import { RelatedTopPostData } from './model';
/**
* The form of the query that gets posted to the analytics/posts WordPress REST
@@ -40,7 +40,7 @@ interface RelatedTopPostsApiResponse {
/**
* The form of the result returned by the getRelatedTopPosts() function.
*/
-interface GetRelatedTopPostsResult {
+export interface GetRelatedTopPostsResult {
message: string;
posts: RelatedTopPostData[];
}
@@ -48,10 +48,10 @@ interface GetRelatedTopPostsResult {
export const RELATED_POSTS_DEFAULT_LIMIT = 5;
export const RELATED_POSTS_DEFAULT_TIME_RANGE = 3; // In days.
-class ContentHelperProvider {
+class RelatedTopPostsProvider {
/**
- * Returns related top-performing posts to the one that is currently being
- * edited within the WordPress Block Editor.
+ * Returns related top posts to the one that is currently being edited
+ * within the WordPress Block Editor.
*
* The 'related' status is determined by the current post's Author, Category
* or tag.
@@ -81,7 +81,7 @@ class ContentHelperProvider {
return Promise.reject( contentHelperError );
}
- // Fetch results from API and set the Content Helper's message.
+ // Fetch results from API and set the message.
let data;
try {
data = await this.fetchRelatedTopPostsFromWpEndpoint( apiQuery );
@@ -90,16 +90,16 @@ class ContentHelperProvider {
}
/* translators: %s: message such as "in category Foo", %d: number of days */
- let message = sprintf( __( 'Top-performing posts %1$s in last %2$d days.', 'wp-parsely' ), apiQuery.message, RELATED_POSTS_DEFAULT_TIME_RANGE );
+ let message = sprintf( __( 'Top posts %1$s in last %2$d days.', 'wp-parsely' ), apiQuery.message, RELATED_POSTS_DEFAULT_TIME_RANGE );
if ( data.length === 0 ) {
- message = `${ __( 'The Parse.ly API did not return any results for top-performing posts', 'wp-parsely' ) } ${ apiQuery.message }.`;
+ message = `${ __( 'The Parse.ly API did not return any results for related top posts', 'wp-parsely' ) } ${ apiQuery.message }.`;
}
return { message, posts: data };
}
/**
- * Fetches the related top-performing posts data from the WordPress REST API.
+ * Fetches the related top posts data from the WordPress REST API.
*
* @param {RelatedTopPostsApiQuery} query
* @return {Promise>} Array of fetched posts.
@@ -109,9 +109,9 @@ class ContentHelperProvider {
try {
response = await apiFetch( {
- path: addQueryArgs( '/wp-parsely/v1/stats/posts', query.query ),
+ path: addQueryArgs( '/wp-parsely/v1/stats/posts', query.query as object ),
} ) as RelatedTopPostsApiResponse;
- } catch ( wpError ) {
+ } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any
return Promise.reject( new ContentHelperError(
wpError.message, wpError.code
) );
@@ -129,7 +129,7 @@ class ContentHelperProvider {
/**
* Builds the query object used in the API for performing the related
- * top-performing posts request.
+ * top posts request.
*
* @param {User} author The post's author.
* @param {Taxonomy} category The post's category.
@@ -174,4 +174,4 @@ class ContentHelperProvider {
}
}
-export default ContentHelperProvider;
+export default RelatedTopPostsProvider;
diff --git a/src/blocks/recommendations/actions.ts b/src/blocks/recommendations/actions.ts
index 7ae618f91..15cc7294f 100644
--- a/src/blocks/recommendations/actions.ts
+++ b/src/blocks/recommendations/actions.ts
@@ -1,4 +1,4 @@
-import { RECOMMENDATIONS_BLOCK_ERROR, RECOMMENDATIONS_BLOCK_LOADED, RECOMMENDATIONS_BLOCK_RECOMMENDATIONS } from './constants';
+import { RecommendationsAction } from './constants';
import { Recommendation } from './models/Recommendation';
interface SetErrorPayload {
@@ -10,15 +10,15 @@ interface SetRecommendationsPayload {
}
export const setError = ( { error }: SetErrorPayload ) => ( {
- type: RECOMMENDATIONS_BLOCK_ERROR,
+ type: RecommendationsAction.Error,
error,
} );
export const setRecommendations = ( { recommendations }: SetRecommendationsPayload ) => ( {
- type: RECOMMENDATIONS_BLOCK_RECOMMENDATIONS,
+ type: RecommendationsAction.Recommendations,
recommendations,
} );
export const setLoaded = () => ( {
- type: RECOMMENDATIONS_BLOCK_LOADED,
+ type: RecommendationsAction.Loaded,
} );
diff --git a/src/blocks/recommendations/class-recommendations-block.php b/src/blocks/recommendations/class-recommendations-block.php
index 3e9029804..972b21e21 100644
--- a/src/blocks/recommendations/class-recommendations-block.php
+++ b/src/blocks/recommendations/class-recommendations-block.php
@@ -62,10 +62,10 @@ public static function register_block(): void {
*
* @since 3.2.0
*
- * @param array $attributes The user-controlled settings for this block.
- * @return string
+ * @param array $attributes The user-controlled settings for this block.
+ * @return string|false
*/
- public static function render_callback( array $attributes ): string {
+ public static function render_callback( array $attributes ) {
/**
* In block.json we define a `viewScript` that is mean to only be loaded
* on the front end. We need to manually enqueue this script here.
@@ -75,7 +75,7 @@ public static function render_callback( array $attributes ): string {
wp_enqueue_script( 'wp-parsely-recommendations-view-script' );
ob_start();
?>
-
+ >
{
+const ParselyRecommendationsFetcher = ( { boost, limit, sort, isEditMode } : ParselyRecommendationsFetcherProps ): JSX.Element | null => {
const { dispatch } = useRecommendationsStore();
const query = {
@@ -58,7 +58,7 @@ const ParselyRecommendationsFetcher = ( { boost, limit, sort, isEditMode } : Par
}
if ( error ) {
- dispatch( setError( { error } ) );
+ dispatch( setError( { error: error as string } ) );
return;
}
diff --git a/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx b/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx
index 3a7857fed..65d492fc8 100644
--- a/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx
+++ b/src/blocks/recommendations/components/parsely-recommendations-inspector-controls.tsx
@@ -12,6 +12,7 @@ import {
TextControl,
ToggleControl,
} from '@wordpress/components';
+import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
@@ -26,14 +27,20 @@ interface ParselyRecommendationsInspectorControlsProps {
const ParselyRecommendationsInspectorControls = ( {
attributes: { boost, imagestyle, limit, openlinksinnewtab, showimages, sort, title },
setAttributes,
-} : ParselyRecommendationsInspectorControlsProps ) => (
-
+} : ParselyRecommendationsInspectorControlsProps ) => {
+ function setImageStyle( value: string ): void {
+ setAttributes( {
+ imagestyle: value === 'original' ? 'original' : 'thumbnail',
+ } );
+ }
+
+ return
setAttributes( { title: newval } ) }
+ onChange={ useCallback( ( value: string ): void => setAttributes( { title: value } ), [ title ] ) }
/>
@@ -41,7 +48,7 @@ const ParselyRecommendationsInspectorControls = ( {
label={ __( 'Maximum Results', 'wp-parsely' ) }
min={ 1 }
max={ 25 }
- onChange={ ( newval ) => setAttributes( { limit: newval } ) }
+ onChange={ useCallback( ( value: number ): void => setAttributes( { limit: value } ), [ limit ] ) }
value={ limit }
/>
@@ -49,7 +56,7 @@ const ParselyRecommendationsInspectorControls = ( {
setAttributes( { openlinksinnewtab: ! openlinksinnewtab } ) }
+ onChange={ useCallback( (): void => setAttributes( { openlinksinnewtab: ! openlinksinnewtab } ), [ openlinksinnewtab ] ) }
/>
@@ -61,7 +68,7 @@ const ParselyRecommendationsInspectorControls = ( {
: __( 'Not showing images', 'wp-parsely' )
}
checked={ showimages }
- onChange={ () => setAttributes( { showimages: ! showimages } ) }
+ onChange={ useCallback( (): void => setAttributes( { showimages: ! showimages } ), [ showimages ] ) }
/>
{ showimages && (
@@ -73,11 +80,7 @@ const ParselyRecommendationsInspectorControls = ( {
{ label: __( 'Original image', 'wp-parsely' ), value: 'original' },
{ label: __( 'Thumbnail from Parse.ly', 'wp-parsely' ), value: 'thumbnail' },
] }
- onChange={ ( newval ) =>
- setAttributes( {
- imagestyle: newval === 'original' ? 'original' : 'thumbnail',
- } )
- }
+ onChange={ setImageStyle }
/>
) }
@@ -95,7 +98,7 @@ const ParselyRecommendationsInspectorControls = ( {
value: 'pub_date',
},
] }
- onChange={ ( newval ) => setAttributes( { sort: newval } ) }
+ onChange={ useCallback( ( value: string ): void => setAttributes( { sort: value } ), [ sort ] ) }
/>
@@ -180,11 +183,11 @@ const ParselyRecommendationsInspectorControls = ( {
value: 'pi_referrals',
},
] }
- onChange={ ( newval ) => setAttributes( { boost: newval } ) }
+ onChange={ useCallback( ( value: string ): void => setAttributes( { boost: value } ), [ boost ] ) }
/>
-
-);
+ ;
+};
export default ParselyRecommendationsInspectorControls;
diff --git a/src/blocks/recommendations/components/parsely-recommendations-list.tsx b/src/blocks/recommendations/components/parsely-recommendations-list.tsx
index 902a6d689..1495ac373 100644
--- a/src/blocks/recommendations/components/parsely-recommendations-list.tsx
+++ b/src/blocks/recommendations/components/parsely-recommendations-list.tsx
@@ -18,11 +18,11 @@ interface ParselyRecommendationsListProps {
const ParselyRecommendationsList = ( { imagestyle, recommendations, showimages, openlinksinnewtab }: ParselyRecommendationsListProps ) => (
- { recommendations.map( ( recommendation, index ) => (
+ { recommendations.map( ( recommendation ) => (
{
- switch ( action.type ) {
- case RECOMMENDATIONS_BLOCK_ERROR:
- return { ...state, isLoaded: true, error: action.error, recommendations: undefined };
- case RECOMMENDATIONS_BLOCK_LOADED:
- return { ...state, isLoaded: true };
- case RECOMMENDATIONS_BLOCK_RECOMMENDATIONS: {
- const { recommendations } = action;
- if ( ! Array.isArray( recommendations ) ) {
- return { ...state, recommendations: undefined };
- }
- const validRecommendations = recommendations.map(
- // eslint-disable-next-line camelcase
- ( { title, url, image_url, thumb_url_medium } ) => ( {
- title,
- url,
- image_url, // eslint-disable-line camelcase
- thumb_url_medium, // eslint-disable-line camelcase
- } )
- );
- return { ...state, isLoaded: true, error: undefined, recommendations: validRecommendations };
- }
- default:
- return { ...state };
- }
-};
-
-const RecommendationsStore = ( props ) => {
- const defaultState = {
- isLoaded: false,
- recommendations: undefined,
- uuid: window.PARSELY?.config?.uuid,
- clientId: props.clientId,
- };
-
- const [ state, dispatch ] = useReducer( reducer, defaultState );
- return ;
-};
-
-export const useRecommendationsStore = () => useContext( RecommendationsContext );
-
-export default RecommendationsStore;
diff --git a/src/blocks/recommendations/recommendations-store.tsx b/src/blocks/recommendations/recommendations-store.tsx
new file mode 100644
index 000000000..77a1ece2f
--- /dev/null
+++ b/src/blocks/recommendations/recommendations-store.tsx
@@ -0,0 +1,71 @@
+/**
+ * External dependencies
+ */
+import { createContext, useContext, useReducer } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { RecommendationsAction } from './constants';
+import { Recommendation } from './models/Recommendation';
+
+interface RecommendationState {
+ isLoaded: boolean;
+ recommendations: Recommendation[];
+ uuid: string | null;
+ clientId: string | null;
+ error: Error | null;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const RecommendationsContext = createContext( {} as any );
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const reducer = ( state: RecommendationState, action: any ): RecommendationState => {
+ switch ( action.type ) {
+ case RecommendationsAction.Error:
+ return { ...state, isLoaded: true, error: action.error, recommendations: [] };
+ case RecommendationsAction.Loaded:
+ return { ...state, isLoaded: true };
+ case RecommendationsAction.Recommendations: {
+ const { recommendations } = action;
+ if ( ! Array.isArray( recommendations ) ) {
+ return { ...state, recommendations: [] };
+ }
+ const validRecommendations = recommendations.map(
+ // eslint-disable-next-line camelcase
+ ( { title, url, image_url, thumb_url_medium } ) => ( {
+ title,
+ url,
+ image_url, // eslint-disable-line camelcase
+ thumb_url_medium, // eslint-disable-line camelcase
+ } )
+ );
+ return { ...state, isLoaded: true, error: null, recommendations: validRecommendations };
+ }
+ default:
+ return { ...state };
+ }
+};
+
+interface RecommendationStore {
+ clientId?: string;
+ children: React.ReactNode;
+}
+
+const RecommendationsStore = ( props: RecommendationStore ) => {
+ const defaultState: RecommendationState = {
+ isLoaded: false,
+ recommendations: [],
+ uuid: window.PARSELY?.config?.uuid || null,
+ clientId: props?.clientId || null,
+ error: null,
+ };
+
+ const [ state, dispatch ] = useReducer( reducer, defaultState );
+ return ;
+};
+
+export const useRecommendationsStore = () => useContext( RecommendationsContext );
+
+export default RecommendationsStore;
diff --git a/src/blocks/recommendations/view.tsx b/src/blocks/recommendations/view.tsx
index 1a1c9fe18..b7dd86263 100644
--- a/src/blocks/recommendations/view.tsx
+++ b/src/blocks/recommendations/view.tsx
@@ -12,11 +12,11 @@ import RecommendationsStore from './recommendations-store';
domReady( () => {
const blocks = document.querySelectorAll( '.wp-block-wp-parsely-recommendations' );
- blocks.forEach( ( block, i ) =>
+ blocks.forEach( ( block ) =>
render(
{ /* @ts-ignore */ }
-
+
,
block
)
diff --git a/src/blocks/shared/functions.ts b/src/blocks/shared/functions.ts
new file mode 100644
index 000000000..7915e640d
--- /dev/null
+++ b/src/blocks/shared/functions.ts
@@ -0,0 +1,55 @@
+/**
+ * Implements the "Imprecise Number" functionality of the Parse.ly dashboard.
+ *
+ * Note: This function is not made to process float numbers.
+ *
+ * @param {string} value The number to process. It can be formatted.
+ * @param {number} fractionDigits The number of desired fraction digits.
+ * @param {string} glue A string to put between the number and unit.
+ * @return {string} The number formatted as an imprecise number.
+ */
+export function formatToImpreciseNumber( value: string, fractionDigits = 1, glue = '' ): string {
+ const number = parseInt( value.replace( /\D/g, '' ), 10 );
+
+ if ( number < 1000 ) {
+ return value;
+ } else if ( number < 10000 ) {
+ fractionDigits = 1;
+ }
+
+ const unitNames: {[key:string]: string} = {
+ 1000: 'k',
+ '1,000,000': 'M',
+ '1,000,000,000': 'B',
+ '1,000,000,000,000': 'T',
+ '1,000,000,000,000,000': 'Q',
+ };
+ let currentNumber = number;
+ let currentNumberAsString = number.toString();
+ let unit = '';
+ let previousNumber = 0;
+
+ Object.entries( unitNames ).forEach( ( [ thousands, suffix ] ) => {
+ const thousandsInt = parseInt( thousands.replace( /\D/g, '' ), 10 );
+
+ if ( number >= thousandsInt ) {
+ currentNumber = number / thousandsInt;
+ let precision = fractionDigits;
+
+ // For over 10 units, we reduce the precision to 1 fraction digit.
+ if ( currentNumber % 1 > 1 / previousNumber ) {
+ precision = currentNumber > 10 ? 1 : 2;
+ }
+
+ // Precision override, where we want to show 2 fraction digits.
+ const zeroes = parseFloat( currentNumber.toFixed( 2 ) ) === parseFloat( currentNumber.toFixed( 0 ) );
+ precision = zeroes ? 0 : precision;
+ currentNumberAsString = currentNumber.toFixed( precision );
+ unit = suffix;
+ }
+
+ previousNumber = thousandsInt;
+ } );
+
+ return currentNumberAsString + glue + unit;
+}
diff --git a/src/blocks/shared/utils/constants.ts b/src/blocks/shared/utils/constants.ts
index 4a2f67938..54966a6a5 100644
--- a/src/blocks/shared/utils/constants.ts
+++ b/src/blocks/shared/utils/constants.ts
@@ -1 +1,2 @@
export const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
+export const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2';
diff --git a/src/blocks/shared/utils/date.ts b/src/blocks/shared/utils/date.ts
index 4e57e0ddb..d0e614e3d 100644
--- a/src/blocks/shared/utils/date.ts
+++ b/src/blocks/shared/utils/date.ts
@@ -1,4 +1,5 @@
export const SHORT_DATE_FORMAT: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' };
+export const SHORT_DATE_FORMAT_WITHOUT_YEAR: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' };
export function getDateInUserLang( date: Date, options: Intl.DateTimeFormatOptions ): string {
return Intl.DateTimeFormat(
@@ -6,3 +7,48 @@ export function getDateInUserLang( date: Date, options: Intl.DateTimeFormatOptio
options
).format( date );
}
+
+/**
+ * Returns the passed date in short format or in short format without year (if
+ * the passed date is within the current year), respecting the user's language.
+ *
+ * @param {Date} date The date to be formatted.
+ * @return {string} The resulting date in its final format.
+ */
+export function getSmartShortDate( date: Date ): string {
+ let dateFormat = SHORT_DATE_FORMAT;
+
+ if ( date.getUTCFullYear() === new Date().getUTCFullYear() ) {
+ dateFormat = SHORT_DATE_FORMAT_WITHOUT_YEAR;
+ }
+
+ return Intl.DateTimeFormat(
+ document.documentElement.lang || 'en',
+ dateFormat
+ ).format( date );
+}
+
+/**
+ * Removes the given number of days from a "YYYY-MM-DD" string, and returns
+ * the result in the same format.
+ *
+ * @param {string} date The date in "YYYY-MM-DD" format.
+ * @param {number} days The number of days to remove from the date.
+ * @return {string} The resulting date in "YYYY-MM-DD" format.
+ */
+export function removeDaysFromDate( date: string, days: number ): string {
+ const pastDate = new Date( date );
+ pastDate.setDate( pastDate.getDate() - days );
+
+ return convertDateToString( pastDate );
+}
+
+/**
+ * Converts a date to a string in "YYYY-MM-DD" format.
+ *
+ * @param {Date} date The date to format.
+ * @return {string} The date in "YYYY-MM-DD" format.
+ */
+export function convertDateToString( date: Date ): string {
+ return date.toISOString().substring( 0, 10 );
+}
diff --git a/src/blocks/shared/utils/post.ts b/src/blocks/shared/utils/post.ts
new file mode 100644
index 000000000..1e6d0f2fa
--- /dev/null
+++ b/src/blocks/shared/utils/post.ts
@@ -0,0 +1,10 @@
+/**
+ * Gets edit url of the post.
+ *
+ * @param {number} postId ID of the post.
+ *
+ * @return {string} Edit url of the post.
+ */
+export function getPostEditUrl( postId: number ): string {
+ return `/wp-admin/post.php?post=${ postId }&action=edit`;
+}
diff --git a/src/blocks/shared/variables.scss b/src/blocks/shared/variables.scss
deleted file mode 100644
index 35cc6fd42..000000000
--- a/src/blocks/shared/variables.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-$html-font-size: 16px;
-
-// Colors
-$black: #000;
-// gray
-$gray-300: #edeeef;
-$gray-600: #586069;
-$gray-700: #444d56;
-// blue
-$blue-550: #2596db;
diff --git a/src/class-dashboard-link.php b/src/class-dashboard-link.php
index b7034beb2..6b9572f8d 100644
--- a/src/class-dashboard-link.php
+++ b/src/class-dashboard-link.php
@@ -27,12 +27,17 @@ class Dashboard_Link {
* @since 3.1.0 Moved to class-dashboard-link.php. Added source parameter.
*
* @param WP_Post $post Which post object or ID to check.
- * @param string $apikey API key or empty string.
+ * @param string $site_id Site ID or empty string.
* @param string $campaign Campaign name for the `utm_campaign` URL parameter.
* @param string $source Source name for the `utm_source` URL parameter.
* @return string
*/
- public static function generate_url( WP_Post $post, string $apikey, string $campaign, string $source ): string {
+ public static function generate_url( WP_Post $post, string $site_id, string $campaign, string $source ): string {
+ /**
+ * Internal variable.
+ *
+ * @var string|false
+ */
$permalink = get_permalink( $post );
if ( ! is_string( $permalink ) ) {
return '';
@@ -45,9 +50,7 @@ public static function generate_url( WP_Post $post, string $apikey, string $camp
'utm_medium' => 'wp-parsely',
);
- $base_url = trailingslashit( Parsely::DASHBOARD_BASE_URL . "/{$apikey}" ) . 'find';
-
- return add_query_arg( $query_args, $base_url );
+ return add_query_arg( $query_args, Parsely::get_dash_url( $site_id ) );
}
/**
@@ -61,6 +64,6 @@ public static function generate_url( WP_Post $post, string $apikey, string $camp
* @return bool True if the link can be shown, false otherwise.
*/
public static function can_show_link( WP_Post $post, Parsely $parsely ): bool {
- return Parsely::post_has_trackable_status( $post ) && is_post_type_viewable( $post->post_type ) && ! $parsely->api_key_is_missing();
+ return Parsely::post_has_trackable_status( $post ) && is_post_type_viewable( $post->post_type ) && ! $parsely->site_id_is_missing();
}
}
diff --git a/src/class-metadata.php b/src/class-metadata.php
index 07d0acd39..f9e3f559d 100644
--- a/src/class-metadata.php
+++ b/src/class-metadata.php
@@ -21,11 +21,48 @@
use Parsely\Metadata\Tag_Builder;
use WP_Post;
+use function Parsely\Utils\get_page_for_posts;
+use function Parsely\Utils\get_page_on_front;
+
/**
* Generates and inserts metadata readable by the Parse.ly Crawler.
*
* @since 1.0.0
* @since 3.3.0 Logic extracted from Parsely\Parsely class to separate file/class.
+ *
+ * @phpstan-type Metadata_Attributes array{
+ * '@id'?: string,
+ * '@type'?: string,
+ * headline?: string,
+ * url?: string,
+ * image?: Metadata_Image,
+ * thumbnailUrl?: string,
+ * articleSection?: string,
+ * creator?: string[],
+ * author?: Metadata_Author[],
+ * publisher?: Metadata_Publisher,
+ * keywords?: string[],
+ * dateCreated?: string,
+ * datePublished?: string,
+ * dateModified?: string,
+ * custom_metadata?: string,
+ * }
+ *
+ * @phpstan-type Metadata_Image array{
+ * '@type': 'ImageObject',
+ * url: string,
+ * }
+ *
+ * @phpstan-type Metadata_Author array{
+ * '@type': 'Person',
+ * name: string,
+ * }
+ *
+ * @phpstan-type Metadata_Publisher array{
+ * '@type': 'Organization',
+ * name: string,
+ * logo: string,
+ * }
*/
class Metadata {
/**
@@ -49,9 +86,9 @@ public function __construct( Parsely $parsely ) {
*
* @param WP_Post $post object.
*
- * @return array
+ * @return Metadata_Attributes
*/
- public function construct_metadata( WP_Post $post ): array {
+ public function construct_metadata( WP_Post $post ) {
$options = $this->parsely->get_options();
$queried_object_id = get_queried_object_id();
@@ -61,12 +98,12 @@ public function construct_metadata( WP_Post $post ): array {
} else {
$builder = new Paginated_Front_Page_Builder( $this->parsely );
}
- } elseif ( 'page' === get_option( 'show_on_front' ) && ! get_option( 'page_on_front' ) ) {
+ } elseif ( 'page' === get_option( 'show_on_front' ) && ! get_page_on_front() ) {
$builder = new Front_Page_Builder( $this->parsely );
} elseif (
is_home() && (
- ! ( 'page' === get_option( 'show_on_front' ) && ! get_option( 'page_on_front' ) ) ||
- $queried_object_id && (int) get_option( 'page_for_posts' ) === $queried_object_id
+ ! ( 'page' === get_option( 'show_on_front' ) && ! get_page_on_front() ) ||
+ get_page_for_posts() === $queried_object_id
)
) {
$builder = new Page_For_Posts_Builder( $this->parsely );
@@ -93,6 +130,8 @@ public function construct_metadata( WP_Post $post ): array {
/**
* Filters the structured metadata.
*
+ * @var mixed
+ *
* @param array $parsely_page Existing structured metadata for a page.
* @param WP_Post $post Post object.
* @param array $options The Parse.ly options.
diff --git a/src/class-parsely.php b/src/class-parsely.php
index 46d16bfc1..e377fbfa8 100644
--- a/src/class-parsely.php
+++ b/src/class-parsely.php
@@ -18,21 +18,47 @@
*
* @since 1.0.0
* @since 2.5.0 Moved from plugin root file to this file.
+ *
+ * @phpstan-type Parsely_Options array{
+ * apikey: string,
+ * content_id_prefix: string,
+ * api_secret: string,
+ * use_top_level_cats: bool,
+ * custom_taxonomy_section: string,
+ * cats_as_tags: bool,
+ * track_authenticated_users: bool,
+ * lowercase_tags: bool,
+ * force_https_canonicals: bool,
+ * track_post_types: string[],
+ * track_page_types: string[],
+ * track_post_types_as?: array,
+ * disable_javascript: bool,
+ * disable_amp: bool,
+ * meta_type: string,
+ * logo: string,
+ * metadata_secret: string,
+ * parsely_wipe_metadata_cache: bool,
+ * disable_autotrack: bool,
+ * plugin_version: string,
+ * }
+ *
+ * @phpstan-import-type Metadata_Attributes from Metadata
*/
class Parsely {
/**
* Declare our constants
*/
- public const VERSION = PARSELY_VERSION;
- public const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php.
- public const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database.
- public const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings.
- public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
+ public const VERSION = PARSELY_VERSION;
+ public const MENU_SLUG = 'parsely'; // Defines the page param passed to options-general.php.
+ public const OPTIONS_KEY = 'parsely'; // Defines the key used to store options in the WP database.
+ public const CAPABILITY = 'manage_options'; // The capability required for the user to administer settings.
+ public const DASHBOARD_BASE_URL = 'https://dash.parsely.com';
+ public const PUBLIC_API_BASE_URL = 'https://api.parsely.com/v2';
/**
* Declare some class properties
*
- * @var array $option_defaults The defaults we need for the class.
+ * @var Parsely_Options $option_defaults The defaults we need for the class.
*/
private $option_defaults = array(
'apikey' => '',
@@ -53,6 +79,7 @@ class Parsely {
'metadata_secret' => '',
'parsely_wipe_metadata_cache' => false,
'disable_autotrack' => false,
+ 'plugin_version' => '',
);
/**
@@ -90,6 +117,21 @@ class Parsely {
'Movie',
);
+ /**
+ * Declare all supported types (both post and non-post types).
+ *
+ * @since 3.7.0
+ * @var string[]
+ */
+ private static $all_supported_types;
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ self::$all_supported_types = array_merge( self::SUPPORTED_JSONLD_POST_TYPES, self::SUPPORTED_JSONLD_NON_POST_TYPES );
+ }
+
/**
* Registers action and filter hook callbacks, and immediately upgrades
* options if needed.
@@ -97,10 +139,16 @@ class Parsely {
public function run(): void {
// Run upgrade options if they exist for the version currently defined.
$options = $this->get_options();
- if ( empty( $options['plugin_version'] ) || self::VERSION !== $options['plugin_version'] ) {
+ if ( self::VERSION !== $options['plugin_version'] ) {
$method = 'upgrade_plugin_to_version_' . str_replace( '.', '_', self::VERSION );
if ( method_exists( $this, $method ) ) {
- call_user_func_array( array( $this, $method ), array( $options ) );
+ /**
+ * Variable.
+ *
+ * @var callable
+ */
+ $callable = array( $this, $method );
+ call_user_func_array( $callable, array( $options ) );
}
// Update our version info.
$options['plugin_version'] = self::VERSION;
@@ -116,8 +164,9 @@ public function run(): void {
/**
* Adds 10 minute cron interval.
*
- * @param array $schedules WP schedules array.
- * @return array
+ * @param array $schedules WP schedules array.
+ *
+ * @return array
*/
public function wpparsely_add_cron_interval( array $schedules ): array {
$schedules['everytenminutes'] = array(
@@ -136,8 +185,8 @@ public function wpparsely_add_cron_interval( array $schedules ): array {
* @return string
*/
public function get_tracker_url(): string {
- if ( $this->api_key_is_set() ) {
- $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_api_key() . '/p.js';
+ if ( $this->site_id_is_set() ) {
+ $tracker_url = 'https://cdn.parsely.com/keys/' . $this->get_site_id() . '/p.js';
return esc_url( $tracker_url );
}
return '';
@@ -237,9 +286,10 @@ public static function post_has_trackable_status( $post ): bool {
*
* @param array $parsely_options parsely_options array.
* @param WP_Post $post object.
- * @return array
+ *
+ * @return Metadata_Attributes
*/
- public function construct_parsely_metadata( array $parsely_options, WP_Post $post ): array {
+ public function construct_parsely_metadata( array $parsely_options, WP_Post $post ) {
_deprecated_function( __FUNCTION__, '3.3', 'Metadata::construct_metadata()' );
$metadata = new Metadata( $this );
return $metadata->construct_metadata( $post );
@@ -252,7 +302,7 @@ public function construct_parsely_metadata( array $parsely_options, WP_Post $pos
*/
public function update_metadata_endpoint( int $post_id ): void {
$parsely_options = $this->get_options();
- if ( $this->api_key_is_missing() || empty( $parsely_options['metadata_secret'] ) ) {
+ if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) {
return;
}
@@ -264,17 +314,17 @@ public function update_metadata_endpoint( int $post_id ): void {
$metadata = ( new Metadata( $this ) )->construct_metadata( $post );
$endpoint_metadata = array(
- 'canonical_url' => $metadata['url'],
- 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ),
- 'title' => $metadata['headline'],
- 'image_url' => $metadata['image']['url'],
- 'pub_date_tmsp' => $metadata['datePublished'],
- 'section' => $metadata['articleSection'],
- 'authors' => $metadata['creator'],
- 'tags' => $metadata['keywords'],
+ 'canonical_url' => $metadata['url'] ?? '',
+ 'page_type' => $this->convert_jsonld_to_parsely_type( $metadata['@type'] ?? '' ),
+ 'title' => $metadata['headline'] ?? '',
+ 'image_url' => isset( $metadata['image']['url'] ) ? $metadata['image']['url'] : '',
+ 'pub_date_tmsp' => $metadata['datePublished'] ?? '',
+ 'section' => $metadata['articleSection'] ?? '',
+ 'authors' => $metadata['creator'] ?? '',
+ 'tags' => $metadata['keywords'] ?? '',
);
- $parsely_api_endpoint = 'https://api.parsely.com/v2/metadata/posts';
+ $parsely_api_endpoint = self::PUBLIC_API_BASE_URL . '/metadata/posts';
$parsely_metadata_secret = $parsely_options['metadata_secret'];
$headers = array(
'Content-Type' => 'application/json',
@@ -282,7 +332,7 @@ public function update_metadata_endpoint( int $post_id ): void {
$body = wp_json_encode(
array(
'secret' => $parsely_metadata_secret,
- 'apikey' => $parsely_options['apikey'],
+ 'apikey' => $this->get_site_id(),
'metadata' => $endpoint_metadata,
)
);
@@ -308,8 +358,7 @@ public function update_metadata_endpoint( int $post_id ): void {
*/
public function bulk_update_posts(): void {
global $wpdb;
- $parsely_options = $this->get_options();
- $allowed_types = array_merge( $parsely_options['track_post_types'], $parsely_options['track_page_types'] );
+ $allowed_types = $this->get_all_track_types();
$allowed_types_string = implode(
', ',
array_map(
@@ -319,7 +368,13 @@ function( $v ) {
$allowed_types
)
);
- $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
+
+ /**
+ * Variable.
+ *
+ * @var int[]|false
+ */
+ $ids = wp_cache_get( 'parsely_post_ids_need_meta_updating' );
if ( false === $ids ) {
$ids = array();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
@@ -350,9 +405,14 @@ function( $v ) {
* As soon as actual options are saved, they override the defaults. This
* prevents us from having to do a lot of isset() checking on variables.
*
- * @return array
+ * @return Parsely_Options
*/
- public function get_options(): array {
+ public function get_options() {
+ /**
+ * Variable.
+ *
+ * @var Parsely_Options|null
+ */
$options = get_option( self::OPTIONS_KEY, $this->option_defaults );
if ( ! is_array( $options ) ) {
@@ -374,6 +434,27 @@ public static function get_settings_url( int $_blog_id = null ): string {
return get_admin_url( $_blog_id, 'options-general.php?page=' . self::MENU_SLUG );
}
+ /**
+ * Returns the URL of the Parse.ly dashboard for a specific page. If a page
+ * is not specified, the home dashboard URL for the specified Site ID is
+ * returned.
+ *
+ * @since 3.7.0
+ *
+ * @param string $site_id The Site ID for which to get the URL.
+ * @param string $page_url Optional. The page for which to get the URL.
+ * @return string The complete dashboard URL.
+ */
+ public static function get_dash_url( string $site_id, string $page_url = '' ): string {
+ $result = trailingslashit( self::DASHBOARD_BASE_URL . '/' . $site_id ) . 'find';
+
+ if ( '' !== $page_url ) {
+ $result .= '?url=' . rawurlencode( $page_url );
+ }
+
+ return $result;
+ }
+
/**
* Checks to see if the current user is a member of the current blog.
*
@@ -406,44 +487,43 @@ public function convert_jsonld_to_parsely_type( string $type ): string {
}
/**
- * Determines if an API key is saved in the options.
+ * Determines if a Site ID is saved in the options.
*
* @since 2.6.0
+ * @since 3.7.0 renamed from api_key_is_set
*
- * @return bool True is API key is set, false if it is missing.
+ * @return bool True is Site ID is set, false if it is missing.
*/
- public function api_key_is_set(): bool {
+ public function site_id_is_set(): bool {
$options = $this->get_options();
- return (
- isset( $options['apikey'] ) &&
- is_string( $options['apikey'] ) &&
- '' !== $options['apikey']
- );
+ return '' !== $options['apikey'];
}
/**
- * Determines if an API key is not saved in the options.
+ * Determines if a Site ID is not saved in the options.
*
* @since 2.6.0
+ * @since 3.7.0 renamed from api_key_is_missing
*
- * @return bool True if API key is missing, false if it is set.
+ * @return bool True if Site ID is missing, false if it is set.
*/
- public function api_key_is_missing(): bool {
- return ! $this->api_key_is_set();
+ public function site_id_is_missing(): bool {
+ return ! $this->site_id_is_set();
}
/**
- * Gets the API key if set.
+ * Gets the Site ID if set.
*
* @since 2.6.0
+ * @since 3.7.0 renamed from get_site_id
*
- * @return string API key if set, or empty string if not.
+ * @return string Site ID if set, or empty string if not.
*/
- public function get_api_key(): string {
+ public function get_site_id(): string {
$options = $this->get_options();
- return $this->api_key_is_set() ? $options['apikey'] : '';
+ return $this->site_id_is_set() ? $options['apikey'] : '';
}
/**
@@ -456,11 +536,7 @@ public function get_api_key(): string {
public function api_secret_is_set(): bool {
$options = $this->get_options();
- return (
- isset( $options['api_secret'] ) &&
- is_string( $options['api_secret'] ) &&
- '' !== $options['api_secret']
- );
+ return '' !== $options['api_secret'];
}
/**
@@ -475,4 +551,28 @@ public function get_api_secret(): string {
return $this->api_secret_is_set() ? $options['api_secret'] : '';
}
+
+ /**
+ * Returns all supported post and non-post types.
+ *
+ * @since 3.7.0
+ *
+ * @return string[] all supported types
+ */
+ public function get_all_supported_types(): array {
+ return self::$all_supported_types;
+ }
+
+ /**
+ * Gets all tracked post types.
+ *
+ * @since 3.7.0
+ *
+ * @return array
+ */
+ public function get_all_track_types(): array {
+ $options = $this->get_options();
+
+ return array_unique( array_merge( $options['track_post_types'], $options['track_page_types'] ) );
+ }
}
diff --git a/src/class-scripts.php b/src/class-scripts.php
index b34ca406c..3eb5b497f 100644
--- a/src/class-scripts.php
+++ b/src/class-scripts.php
@@ -40,14 +40,14 @@ public function __construct( Parsely $parsely ) {
*/
public function run(): void {
$parsely_options = $this->parsely->get_options();
- if ( $this->parsely->api_key_is_set() && true !== $parsely_options['disable_javascript'] ) {
+ if ( $this->parsely->site_id_is_set() && true !== $parsely_options['disable_javascript'] ) {
add_action( 'init', array( $this, 'register_scripts' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_js_tracker' ) );
}
}
/**
- * Registers scripts, if there's an API key value saved.
+ * Registers scripts, if there's a Site ID value saved.
*
* @since 2.5.0
* @since 3.0.0 Rename from register_js
@@ -61,12 +61,12 @@ public function register_scripts(): void {
true
);
- $loader_asset = require plugin_dir_path( PARSELY_FILE ) . 'build/loader.asset.php';
+ $loader_asset = require_once plugin_dir_path( PARSELY_FILE ) . 'build/loader.asset.php';
wp_register_script(
'wp-parsely-loader',
plugin_dir_url( PARSELY_FILE ) . 'build/loader.js',
- $loader_asset['dependencies'],
- $loader_asset['version'],
+ $loader_asset['dependencies'] ?? null,
+ $loader_asset['version'] ?? Parsely::VERSION,
true
);
}
@@ -116,14 +116,14 @@ public function enqueue_js_tracker(): void {
wp_enqueue_script( 'wp-parsely-loader' );
wp_enqueue_script( 'wp-parsely-tracker' );
- // If we don't have an API secret, there's no need to set the API key.
- // Setting the API key triggers the UUID Profile Call function.
- if ( isset( $parsely_options['api_secret'] ) && is_string( $parsely_options['api_secret'] ) && '' !== $parsely_options['api_secret'] ) {
- $js_api_key = "window.wpParselyApiKey = '" . esc_js( $this->parsely->get_api_key() ) . "';";
- wp_add_inline_script( 'wp-parsely-loader', $js_api_key, 'before' );
+ // If we don't have an API secret, there's no need to set the Site ID.
+ // Setting the Site ID triggers the UUID Profile Call function.
+ if ( $this->parsely->api_secret_is_set() ) {
+ $js_site_id = "window.wpParselySiteId = '" . esc_js( $this->parsely->get_site_id() ) . "';";
+ wp_add_inline_script( 'wp-parsely-loader', $js_site_id, 'before' );
}
- if ( isset( $parsely_options['disable_autotrack'] ) && true === $parsely_options['disable_autotrack'] ) {
+ if ( true === $parsely_options['disable_autotrack'] ) {
$disable_autotrack = 'window.wpParselyDisableAutotrack = true;';
wp_add_inline_script( 'wp-parsely-loader', $disable_autotrack, 'before' );
}
@@ -140,7 +140,6 @@ public function enqueue_js_tracker(): void {
* @return string Amended `script` tag.
*/
public function script_loader_tag( string $tag, string $handle, string $src ): string {
- $parsely_options = $this->parsely->get_options();
if ( \in_array(
$handle,
array(
@@ -167,11 +166,14 @@ public function script_loader_tag( string $tag, string $handle, string $src ): s
if ( null !== $tag && 'wp-parsely-tracker' === $handle ) {
$tag = preg_replace( '/ id=(["\'])wp-parsely-tracker-js\1/', ' id="parsely-cfg"', $tag );
- $tag = str_replace(
- ' src=',
- ' data-parsely-site="' . esc_attr( $parsely_options['apikey'] ) . '" src=',
- $tag
- );
+
+ if ( null !== $tag ) {
+ $tag = str_replace(
+ ' src=',
+ ' data-parsely-site="' . esc_attr( $this->parsely->get_site_id() ) . '" src=',
+ $tag
+ );
+ }
}
return $tag ?? '';
diff --git a/src/content-helper/dashboard-widget/class-dashboard-widget.php b/src/content-helper/dashboard-widget/class-dashboard-widget.php
new file mode 100644
index 000000000..541f2ab33
--- /dev/null
+++ b/src/content-helper/dashboard-widget/class-dashboard-widget.php
@@ -0,0 +1,85 @@
+is_user_allowed_to_make_api_call() ) {
+ return;
+ }
+
+ add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
+ }
+
+ /**
+ * Adds the Widget and its contents to the WordPress Dashboard.
+ *
+ * @since 3.7.0
+ */
+ public function add_dashboard_widget(): void {
+ wp_add_dashboard_widget(
+ 'wp-parsely-dashboard-widget',
+ __( 'Parse.ly Top Posts (Last 7 Days)', 'wp-parsely' ),
+ '__return_empty_string' // Content will be populated by JavaScript.
+ );
+ }
+
+ /**
+ * Enqueues the Dashboard Widget's assets.
+ *
+ * @since 3.7.0
+ *
+ * @param string $hook_suffix The current admin page.
+ */
+ public function enqueue_assets( $hook_suffix ): void {
+ if ( 'index.php' === $hook_suffix ) {
+ $asset_php = require_once plugin_dir_path( PARSELY_FILE ) . 'build/content-helper/dashboard-widget.asset.php';
+ $built_assets_url = plugin_dir_url( PARSELY_FILE ) . 'build/content-helper/';
+
+ wp_enqueue_script(
+ 'wp-parsely-dashboard-widget',
+ $built_assets_url . 'dashboard-widget.js',
+ $asset_php['dependencies'] ?? null,
+ $asset_php['version'] ?? Parsely::VERSION,
+ true
+ );
+
+ wp_enqueue_style(
+ 'wp-parsely-dashboard-widget',
+ $built_assets_url . 'dashboard-widget.css',
+ array(),
+ $asset_php['version'] ?? Parsely::VERSION
+ );
+ }
+ }
+
+}
diff --git a/src/content-helper/dashboard-widget/dashboard-widget.scss b/src/content-helper/dashboard-widget/dashboard-widget.scss
new file mode 100644
index 000000000..aff7614c4
--- /dev/null
+++ b/src/content-helper/dashboard-widget/dashboard-widget.scss
@@ -0,0 +1,125 @@
+@import "../../css/shared/variables";
+@import "../../css/shared/functions";
+
+#wp-parsely-dashboard-widget {
+
+ .parsely-spinner-wrapper {
+ display: flex;
+ justify-content: center;
+ margin: to_rem(103px) 0;
+
+ svg {
+ height: 22px;
+ width: 22px;
+ }
+ }
+
+ .parsely-contact-us {
+ margin-top: to_rem(15px) !important;
+ }
+
+ p.parsely-error-hint {
+ color: var(--gray-700);
+ }
+}
+
+#wp-parsely-dashboard-widget .parsely-top-posts-wrapper {
+ font-family: var(--base-font);
+ color: var(--base-text);
+
+ .page-views-title {
+ margin-bottom: to_rem(4px);
+ text-align: right;
+ width: 100%;
+ }
+
+ .parsely-top-post-content {
+ display: flex;
+
+ // Number at left of thumbnails.
+ &::before {
+ content: counter(item) "";
+ counter-increment: item;
+ padding-right: to_rem(8px);
+ }
+
+ @media only screen and (max-width: 380px) {
+
+ &::before {
+ content: "";
+ padding-right: 0;
+ }
+ }
+ }
+
+ .parsely-top-posts {
+ counter-reset: item; // Needed to increment post numbers.
+ list-style: none;
+ margin: 0;
+ }
+
+ .parsely-top-post {
+ margin-bottom: to_rem(16px);
+ }
+
+ .parsely-top-post-thumbnail {
+ height: 46px;
+ width: 46px;
+
+ img {
+ height: 100%;
+ width: 100%;
+ }
+ }
+
+ .parsely-top-post-data {
+ border-top: 1px solid var(--gray-300);
+ flex-grow: 1; // Take all remaining width.
+ margin-left: to_rem(8px);
+ padding-top: to_rem(4px);
+ }
+
+ // This element can be a link or div.
+ .parsely-top-post-title {
+ color: var(--base-text);
+ font-size: to_rem(14px);
+ margin-right: to_rem(7px);
+ }
+
+ a.parsely-top-post-title:hover {
+ color: var(--blue-550);
+ }
+
+ .parsely-top-post-icon-link {
+ position: relative;
+ top: to_rem(4px);
+
+ svg {
+ fill: #8d98a1;
+ margin-right: to_rem(3px);
+
+ &:hover {
+ fill: var(--blue-550);
+ }
+ }
+ }
+
+ .parsely-top-post-metadata {
+ margin: to_rem(4px) 0 0;
+
+ >span {
+ color: var(--gray-500);
+
+ &:not(:first-child) {
+ margin-left: to_rem(12px);
+ }
+ }
+ }
+
+ .parsely-top-post-views {
+ float: right;
+ font-family: var(--numeric-font);
+ font-size: to_rem(18px);
+ padding-left: to_rem(10px);
+ }
+}
diff --git a/src/content-helper/dashboard-widget/dashboard-widget.tsx b/src/content-helper/dashboard-widget/dashboard-widget.tsx
new file mode 100644
index 000000000..efc3dcf92
--- /dev/null
+++ b/src/content-helper/dashboard-widget/dashboard-widget.tsx
@@ -0,0 +1,20 @@
+/**
+ * External dependencies
+ */
+import { render } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import TopPostList from './top-posts/component-list';
+
+window.addEventListener(
+ 'load',
+ function() {
+ render(
+ ,
+ document.querySelector( '#wp-parsely-dashboard-widget > .inside' )
+ );
+ },
+ false
+);
diff --git a/src/content-helper/dashboard-widget/provider.ts b/src/content-helper/dashboard-widget/provider.ts
new file mode 100644
index 000000000..e597d005d
--- /dev/null
+++ b/src/content-helper/dashboard-widget/provider.ts
@@ -0,0 +1,106 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { addQueryArgs } from '@wordpress/url';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import {
+ ContentHelperError,
+ ContentHelperErrorCode,
+} from '../../blocks/content-helper/content-helper-error';
+import { TopPostData } from './top-posts/model';
+import {
+ convertDateToString,
+ removeDaysFromDate,
+} from '../../blocks/shared/utils/date';
+
+/**
+ * The form of the response returned by the /stats/posts WordPress REST API
+ * endpoint.
+ */
+interface TopPostsApiResponse {
+ error?: Error;
+ data?: TopPostData[];
+}
+
+export const TOP_POSTS_DEFAULT_LIMIT = 3;
+export const TOP_POSTS_DEFAULT_TIME_RANGE = 7; // In days.
+
+class DashboardWidgetProvider {
+ private dataPeriodStart: string;
+ private dataPeriodEnd: string;
+
+ /**
+ * Constructor.
+ */
+ constructor() {
+ this.dataPeriodEnd = convertDateToString( new Date() ) + 'T23:59';
+ this.dataPeriodStart = removeDaysFromDate(
+ this.dataPeriodEnd,
+ TOP_POSTS_DEFAULT_TIME_RANGE - 1
+ ) + 'T00:00';
+ }
+
+ /**
+ * Returns the site's top posts.
+ *
+ * @return {Promise>} Object containing message and posts.
+ */
+ public async getTopPosts(): Promise {
+ let data: TopPostData[] = [];
+
+ try {
+ data = await this.fetchTopPostsFromWpEndpoint();
+ } catch ( contentHelperError ) {
+ return Promise.reject( contentHelperError );
+ }
+
+ if ( 0 === data.length ) {
+ return Promise.reject( new ContentHelperError(
+ __( 'No Top Posts data is available.', 'wp-parsely' ),
+ ContentHelperErrorCode.ParselyApiReturnedNoData,
+ ''
+ ) );
+ }
+
+ return data;
+ }
+
+ /**
+ * Fetches the site's top posts data from the WordPress REST API.
+ *
+ * @return {Promise>} Array of fetched posts.
+ */
+ private async fetchTopPostsFromWpEndpoint(): Promise {
+ let response;
+
+ try {
+ response = await apiFetch( {
+ path: addQueryArgs( '/wp-parsely/v1/stats/posts', {
+ limit: TOP_POSTS_DEFAULT_LIMIT,
+ period_start: this.dataPeriodStart,
+ period_end: this.dataPeriodEnd,
+ } ),
+ } ) as TopPostsApiResponse;
+ } catch ( wpError: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any
+ return Promise.reject( new ContentHelperError(
+ wpError.message, wpError.code
+ ) );
+ }
+
+ if ( response?.error ) {
+ return Promise.reject( new ContentHelperError(
+ response.error.message,
+ ContentHelperErrorCode.ParselyApiResponseContainsError
+ ) );
+ }
+
+ return response?.data || [];
+ }
+}
+
+export default DashboardWidgetProvider;
diff --git a/src/content-helper/dashboard-widget/top-posts/component-list-item.tsx b/src/content-helper/dashboard-widget/top-posts/component-list-item.tsx
new file mode 100644
index 000000000..a22e2c4bf
--- /dev/null
+++ b/src/content-helper/dashboard-widget/top-posts/component-list-item.tsx
@@ -0,0 +1,124 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { TopPostData } from './model';
+import { formatToImpreciseNumber } from '../../../blocks/shared/functions';
+import OpenLinkIcon from '../../../blocks/content-helper/icons/open-link-icon';
+import { getSmartShortDate } from '../../../blocks/shared/utils/date';
+import EditIcon from '../../../blocks/content-helper/icons/edit-icon';
+import { getPostEditUrl } from '../../../blocks/shared/utils/post';
+
+interface TopPostListItemProps {
+ post: TopPostData;
+}
+
+/**
+ * Returns a single list item depicting a post.
+ *
+ * @param {TopPostData} post The Post to be shown.
+ */
+function TopPostListItem( { post }: TopPostListItemProps ): JSX.Element {
+ return (
+
+
+
+ { getPostThumbnailElement( { post } ) }
+
+
+
+
+
+ );
+}
+
+/**
+ * Returns the Post thumbnail with its div container. Returns an empty div if
+ * the post has no thumbnail.
+ *
+ * @param {TopPostData} post The Post from which to get the data.
+ */
+function getPostThumbnailElement( { post }: TopPostListItemProps ): JSX.Element {
+ if ( post.thumbUrlMedium ) {
+ return (
+
+
{ __( 'Thumbnail', 'wp-parsely' ) }
+
+
+ );
+ }
+
+ return (
+
+ {
+ __( 'Post thumbnail not available', 'wp-parsely' )
+ }
+
+ );
+}
+
+/**
+ * Returns the Post title as a link (for editing the Post) or a div if the Post
+ * has no valid ID.
+ *
+ * @param {TopPostData} post The Post from which to get the data.
+ */
+function getPostTitleElement( { post }: TopPostListItemProps ): JSX.Element {
+ return (
+
+
+ { __( 'View in Parse.ly (opens in new tab)', 'wp-parsely' ) }
+
+ { post.title }
+
+ );
+}
+
+export default TopPostListItem;
diff --git a/src/content-helper/dashboard-widget/top-posts/component-list.tsx b/src/content-helper/dashboard-widget/top-posts/component-list.tsx
new file mode 100644
index 000000000..9bb17e8b2
--- /dev/null
+++ b/src/content-helper/dashboard-widget/top-posts/component-list.tsx
@@ -0,0 +1,93 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Spinner } from '@wordpress/components';
+import { useEffect, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import DashboardWidgetProvider from '../provider';
+import TopPostListItem from './component-list-item';
+import { TopPostData } from './model';
+import { ContentHelperError } from '../../../blocks/content-helper/content-helper-error';
+import { getDateInUserLang, SHORT_DATE_FORMAT } from '../../../blocks/shared/utils/date';
+
+const FETCH_RETRIES = 3;
+
+/**
+ * List of the top posts.
+ */
+function TopPostList() {
+ const [ loading, setLoading ] = useState( true );
+ const [ error, setError ] = useState();
+ const [ posts, setPosts ] = useState( [] );
+ const provider = new DashboardWidgetProvider();
+
+ useEffect( () => {
+ const fetchPosts = async ( retries: number ) => {
+ provider.getTopPosts()
+ .then( ( result ): void => {
+ const mappedPosts: TopPostData[] = result.map(
+ ( post: TopPostData ): TopPostData => (
+ {
+ ...post,
+ date: getDateInUserLang( new Date( post.date ), SHORT_DATE_FORMAT ),
+ }
+ )
+ );
+
+ setPosts( mappedPosts );
+ setLoading( false );
+ } )
+ .catch( async ( err ) => {
+ if ( retries > 0 ) {
+ await new Promise( ( r ) => setTimeout( r, 500 ) );
+ await fetchPosts( retries - 1 );
+ } else {
+ setLoading( false );
+ setError( err );
+ }
+ } );
+ };
+
+ setLoading( true );
+ fetchPosts( FETCH_RETRIES );
+
+ return (): void => {
+ setLoading( false );
+ setPosts( [] );
+ setError( undefined );
+ };
+ }, [] );
+
+ // Show error message.
+ if ( error ) {
+ return error.ProcessedMessage( 'parsely-top-posts-descr' );
+ }
+
+ // Show top posts list.
+ const postList: JSX.Element = (
+
+ { posts.map( ( post: TopPostData ): JSX.Element => ) }
+
+ );
+
+ return (
+ loading
+ ? (
+
+
+
+ )
+ : (
+
+
{ __( 'Page Views', 'wp-parsely' ) }
+ { postList }
+
+ )
+ );
+}
+
+export default TopPostList;
diff --git a/src/content-helper/dashboard-widget/top-posts/model.ts b/src/content-helper/dashboard-widget/top-posts/model.ts
new file mode 100644
index 000000000..b8bda7184
--- /dev/null
+++ b/src/content-helper/dashboard-widget/top-posts/model.ts
@@ -0,0 +1,11 @@
+export interface TopPostData {
+ author: string;
+ dashUrl: string;
+ date: string;
+ id: number;
+ postId: number;
+ thumbUrlMedium: string;
+ title: string;
+ url: string;
+ views: number;
+}
diff --git a/src/css/admin-parsely-stats.scss b/src/css/admin-parsely-stats.scss
new file mode 100644
index 000000000..2fb0480f8
--- /dev/null
+++ b/src/css/admin-parsely-stats.scss
@@ -0,0 +1,21 @@
+.column-parsely-stats {
+ width: 200px;
+
+ @media only screen and (max-width: 991px) {
+ width: 150px;
+ }
+
+ .parsely-post-stats {
+ color: #959da5;
+ min-height: 54px;
+ line-height: 18px;
+ }
+
+ .parsely-post-stats-placeholder {
+ letter-spacing: 2px;
+ }
+
+ .parsely-post-page-views {
+ color: #000;
+ }
+}
diff --git a/src/css/admin-settings.css b/src/css/admin-settings.scss
similarity index 100%
rename from src/css/admin-settings.css
rename to src/css/admin-settings.scss
diff --git a/src/css/recommended-widget.css b/src/css/recommended-widget.scss
similarity index 100%
rename from src/css/recommended-widget.css
rename to src/css/recommended-widget.scss
diff --git a/src/blocks/shared/functions.scss b/src/css/shared/functions.scss
similarity index 100%
rename from src/blocks/shared/functions.scss
rename to src/css/shared/functions.scss
diff --git a/src/blocks/content-helper/variables.scss b/src/css/shared/variables.scss
similarity index 78%
rename from src/blocks/content-helper/variables.scss
rename to src/css/shared/variables.scss
index 51dbf79c9..250ced092 100644
--- a/src/blocks/content-helper/variables.scss
+++ b/src/css/shared/variables.scss
@@ -1,20 +1,27 @@
+/** SASS variables **/
+$html-font-size: 16px; // Used in functions.scss.
+
/**
* This is a subset of the CSS variables defined in the Parse.ly dashboard. It
* is sourced from 1.44/src/styles/base.scss and defines some additional
* variables in the end of the file.
*/
-
-.wp-parsely-content-helper {
+.wp-parsely-content-helper,
+#wp-parsely-dashboard-widget {
/** Layout section - base.scss. **/
--base-font: "source-sans-pro", arial, sans-serif;
--numeric-font: "ff-din-round-web", sans-serif;
/** Category colors section - base scss. **/
+ --gray-300: #edeeef;
--gray-400: #d7dbdf;
+ --gray-500: #959da5;
--gray-600: #586069;
--gray-700: #444d56;
+ --gray-900: #24292e;
--blue-500: #44a8e5;
+ --blue-550: #2596db;
--green-500: #7bc01b;
// ref-* variables to be used as HSL colors.
--ref-direct: 205, 13%, 52%;
@@ -24,6 +31,7 @@
--ref-other: 3, 76%, 58%;
/** Theme colors section - base.scss. **/
+ --base-text: var(--gray-900);
--base-text-2: var(--gray-600);
--base-3: var(--gray-400);
--border: var(--gray-400);
@@ -33,5 +41,6 @@
/** Additional variables. **/
--font-size--large: 1rem;
--font-size--extra-large: 1.2rem;
+ --black: #000;
--sidebar-black: #1e1e1e;
}
diff --git a/src/js/admin-parsely-stats.ts b/src/js/admin-parsely-stats.ts
new file mode 100644
index 000000000..e5a5905d4
--- /dev/null
+++ b/src/js/admin-parsely-stats.ts
@@ -0,0 +1,115 @@
+import { ParselyAPIError, ParselyAPIErrorInfo } from './common.interface';
+
+export interface ParselyPostsStatsResponse extends ParselyAPIError {
+ data: ParselyStatsMap | null;
+}
+
+interface ParselyStats {
+ page_views?: string;
+ visitors?: string;
+ avg_time?: string;
+}
+
+interface ParselyStatsMap {
+ [key: string]: ParselyStats;
+}
+
+document.addEventListener( 'DOMContentLoaded', (): void => {
+ showParselyPostsStatsResponse();
+} );
+
+/**
+ * Shows Parse.ly Post Stats or Error depending on response.
+ */
+export function showParselyPostsStatsResponse(): void {
+ updateParselyStatsPlaceholder();
+
+ if ( ! window.wpParselyPostsStatsResponse ) {
+ return;
+ }
+
+ const response: ParselyPostsStatsResponse = JSON.parse( window.wpParselyPostsStatsResponse );
+
+ if ( response?.error ) {
+ showParselyStatsError( response.error );
+ return;
+ }
+
+ if ( response?.data ) {
+ showParselyStats( response.data );
+ }
+}
+
+/**
+ * Replaces Parse.ly Stats placeholder from default to differentiate while the API request
+ * is in progress or completed.
+ */
+function updateParselyStatsPlaceholder(): void {
+ getAllPostStatsElements()?.forEach( ( statsElement: Element ): void => {
+ statsElement.innerHTML = '—';
+ } );
+}
+
+/**
+ * Shows Parse.ly Stats on available posts.
+ *
+ * @param {ParselyStatsMap} parselyStatsMap Object contains unique keys and Parse.ly Stats for posts.
+ */
+function showParselyStats( parselyStatsMap: ParselyStatsMap ): void {
+ if ( ! parselyStatsMap ) {
+ return;
+ }
+
+ getAllPostStatsElements()?.forEach( ( statsElement: Element ): void => {
+ const statsKey = statsElement.getAttribute( 'data-stats-key' );
+
+ if ( statsKey === null || parselyStatsMap[ statsKey ] === undefined ) {
+ return;
+ }
+
+ const stats: ParselyStats = parselyStatsMap[ statsKey ];
+ statsElement.innerHTML = '';
+
+ if ( stats.page_views ) {
+ statsElement.innerHTML += `${ stats.page_views } `;
+ }
+
+ if ( stats.visitors ) {
+ statsElement.innerHTML += `${ stats.visitors } `;
+ }
+
+ if ( stats.avg_time ) {
+ statsElement.innerHTML += `${ stats.avg_time } `;
+ }
+ } );
+}
+
+/**
+ * Shows Parse.ly Stats error as WP Admin Error Notice.
+ *
+ * @param {ParselyAPIErrorInfo} parselyStatsError Object which contians info about error.
+ */
+function showParselyStatsError( parselyStatsError: ParselyAPIErrorInfo ): void {
+ const headerEndElement = document.querySelector( '.wp-header-end' ); // WP has this element before admin notices.
+ if ( headerEndElement === null ) {
+ return;
+ }
+
+ headerEndElement.innerHTML += getWPAdminError( parselyStatsError.htmlMessage );
+}
+
+/**
+ * Gets all elements inside which we will show Parse.ly Stats.
+ */
+function getAllPostStatsElements(): NodeListOf {
+ return document.querySelectorAll( '.parsely-post-stats' );
+}
+
+/**
+ * Gets HTML for showing error message as WP Admin Error Notice.
+ *
+ * @param {string} htmlMessage Message to show inside notice.
+ */
+function getWPAdminError( htmlMessage = '' ): string {
+ return `${ htmlMessage }
`;
+}
diff --git a/src/js/admin-settings.js b/src/js/admin-settings.js
deleted file mode 100644
index 359b798bf..000000000
--- a/src/js/admin-settings.js
+++ /dev/null
@@ -1,20 +0,0 @@
-document.querySelector( '.media-single-image button.browse' ).addEventListener( 'click', selectImage );
-
-function selectImage() {
- const optionName = this.dataset.option;
-
- const imageFrame = wp.media( {
- multiple: false,
- library: {
- type: 'image',
- },
- } );
-
- imageFrame.on( 'select', function() {
- const url = imageFrame.state().get( 'selection' ).first().toJSON().url;
- const inputSelector = '#media-single-image-' + optionName + ' input.file-path';
- document.querySelector( inputSelector ).value = url;
- } );
-
- imageFrame.open();
-}
diff --git a/src/js/admin-settings.ts b/src/js/admin-settings.ts
new file mode 100644
index 000000000..a702f6c16
--- /dev/null
+++ b/src/js/admin-settings.ts
@@ -0,0 +1,24 @@
+document.querySelector( '.media-single-image button.browse' )?.addEventListener( 'click', selectImage );
+
+function selectImage( event: Event ) {
+ const optionName = ( event.target as HTMLButtonElement ).dataset.option;
+
+ const imageFrame = window.wp.media( {
+ multiple: false,
+ library: {
+ type: 'image',
+ },
+ } );
+
+ imageFrame.on( 'select', function() {
+ const url = imageFrame.state().get( 'selection' ).first().toJSON().url;
+ const inputSelector: string = '#media-single-image-' + optionName + ' input.file-path';
+
+ const inputElement: HTMLInputElement | null = document.querySelector( inputSelector );
+ if ( inputElement ) {
+ inputElement.value = url;
+ }
+ } );
+
+ imageFrame.open();
+}
diff --git a/src/js/common.interface.ts b/src/js/common.interface.ts
new file mode 100644
index 000000000..d9af186d5
--- /dev/null
+++ b/src/js/common.interface.ts
@@ -0,0 +1,9 @@
+export interface ParselyAPIError {
+ error: ParselyAPIErrorInfo | null;
+}
+
+export interface ParselyAPIErrorInfo {
+ code: number;
+ message: string;
+ htmlMessage: string;
+}
diff --git a/src/js/lib/loader.js b/src/js/lib/loader.ts
similarity index 90%
rename from src/js/lib/loader.js
rename to src/js/lib/loader.ts
index 95051d83e..cf5659547 100644
--- a/src/js/lib/loader.js
+++ b/src/js/lib/loader.ts
@@ -8,14 +8,14 @@ export function wpParselyInitCustom() {
* All functions enqueued on that hook will be executed on that event according to their priorities. Those
* functions should not expect any parameters and shouldn't return any.
*/
- const customOnLoad = () => window.wpParselyHooks.doAction( 'wpParselyOnLoad' );
+ const customOnLoad = () => window.wpParselyHooks?.doAction( 'wpParselyOnLoad' );
/**
* The `wpParselyOnReady` hook gets called with the `onReady` event of the `window.PARSELY` object.
* All functions enqueued on that hook will be executed on that event according to their priorities. Those
* functions should not expect any parameters and shouldn't return any.
*/
- const customOnReady = () => window.wpParselyHooks.doAction( 'wpParselyOnReady' );
+ const customOnReady = () => window.wpParselyHooks?.doAction( 'wpParselyOnReady' );
// Construct window.PARSELY object.
if ( typeof window.PARSELY === 'object' ) {
diff --git a/src/js/lib/personalization.js b/src/js/lib/personalization.ts
similarity index 100%
rename from src/js/lib/personalization.js
rename to src/js/lib/personalization.ts
diff --git a/src/js/lib/uuid-profile-call.js b/src/js/lib/uuid-profile-call.js
deleted file mode 100644
index 434967e58..000000000
--- a/src/js/lib/uuid-profile-call.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// Only enqueuing the action if the site has a defined API key.
-if ( typeof window.wpParselyApiKey !== 'undefined' ) {
- window.wpParselyHooks.addAction( 'wpParselyOnLoad', 'wpParsely', uuidProfileCall );
-}
-
-async function uuidProfileCall() {
- const uuid = global.PARSELY?.config?.parsely_site_uuid;
-
- if ( ! ( window.wpParselyApiKey && uuid ) ) {
- return;
- }
-
- const url = `https://api.parsely.com/v2/profile?apikey=${ encodeURIComponent(
- window.wpParselyApiKey
- ) }&uuid=${ encodeURIComponent( uuid ) }&url=${ encodeURIComponent( window.location.href ) }`;
-
- return fetch( url );
-}
diff --git a/src/js/lib/uuid-profile-call.ts b/src/js/lib/uuid-profile-call.ts
new file mode 100644
index 000000000..a1c6eadaf
--- /dev/null
+++ b/src/js/lib/uuid-profile-call.ts
@@ -0,0 +1,20 @@
+import { PUBLIC_API_BASE_URL } from '../../blocks/shared/utils/constants';
+
+// Only enqueuing the action if the site has a defined Site ID.
+if ( typeof window.wpParselySiteId !== 'undefined' ) {
+ window.wpParselyHooks?.addAction( 'wpParselyOnLoad', 'wpParsely', uuidProfileCall );
+}
+
+async function uuidProfileCall() {
+ const uuid = window.PARSELY?.config?.parsely_site_uuid;
+
+ if ( ! ( window.wpParselySiteId && uuid ) ) {
+ return;
+ }
+
+ const url = `${ PUBLIC_API_BASE_URL }/profile?apikey=${ encodeURIComponent(
+ window.wpParselySiteId
+ ) }&uuid=${ encodeURIComponent( uuid ) }&url=${ encodeURIComponent( window.location.href ) }`;
+
+ return fetch( url );
+}
diff --git a/src/js/widgets/recommended.js b/src/js/widgets/recommended.ts
similarity index 75%
rename from src/js/widgets/recommended.js
rename to src/js/widgets/recommended.ts
index 593d4d5a7..552cba25f 100644
--- a/src/js/widgets/recommended.js
+++ b/src/js/widgets/recommended.ts
@@ -8,7 +8,34 @@ import domReady from '@wordpress/dom-ready';
*/
import { getUuidFromVisitorCookie } from '../lib/personalization';
-function constructUrl( apiUrl, permalink, personalized ) {
+interface WidgetData {
+ data: {
+ [key: string]: WidgetRecommendation;
+ };
+}
+
+interface WidgetRecommendation {
+ title: string;
+ url: string;
+ author: string;
+ image_url: string;
+ thumb_url_medium: string;
+}
+
+interface WidgetOptions {
+ url: string;
+ outerDiv: Element;
+ displayAuthor: boolean;
+ displayDirection: string | null;
+ imgDisplay: string | null;
+ widgetId: string | null;
+}
+
+interface WidgetOptionsGroup {
+ [key: string]: WidgetOptions[];
+}
+
+function constructUrl( apiUrl: string, permalink: string, personalized: boolean ): string {
if ( personalized ) {
const uuid = getUuidFromVisitorCookie();
if ( uuid ) {
@@ -19,9 +46,9 @@ function constructUrl( apiUrl, permalink, personalized ) {
return `${ apiUrl }&url=${ encodeURIComponent( permalink ) }`;
}
-function constructWidget( widget ) {
- const apiUrl = widget.getAttribute( 'data-parsely-widget-api-url' );
- const permalink = widget.getAttribute( 'data-parsely-widget-permalink' );
+function constructWidget( widget: Element ): WidgetOptions {
+ const apiUrl = widget.getAttribute( 'data-parsely-widget-api-url' ) || '';
+ const permalink = widget.getAttribute( 'data-parsely-widget-permalink' ) || '';
const personalized = widget.getAttribute( 'data-parsely-widget-personalized' ) === 'true';
const url = constructUrl( apiUrl, permalink, personalized );
@@ -35,13 +62,13 @@ function constructWidget( widget ) {
};
}
-function renderWidget( data, {
+function renderWidget( data: WidgetData, {
outerDiv,
displayAuthor,
displayDirection,
imgDisplay,
widgetId,
-} ) {
+}: WidgetOptions ) {
if ( imgDisplay !== 'none' ) {
outerDiv.classList.add( 'display-thumbnail' );
}
@@ -99,14 +126,14 @@ function renderWidget( data, {
}
outerDiv.appendChild( outerList );
- outerDiv.closest( '.widget.Recommended_Widget' ).classList.remove( 'parsely-recommended-widget-hidden' );
+ outerDiv.closest( '.widget.Recommended_Widget' )?.classList.remove( 'parsely-recommended-widget-hidden' );
}
domReady( () => {
const widgetDOMElements = document.querySelectorAll( '.parsely-recommended-widget' );
- const widgetObjects = Array.from( widgetDOMElements ).map( constructWidget );
+ const widgetObjects = Array.from( widgetDOMElements ).map( ( widget: Element ) => constructWidget( widget ) );
- const widgetsGroupedByUrl = widgetObjects.reduce( ( acc, curr ) => {
+ const widgetsGroupedByUrl: WidgetOptionsGroup = widgetObjects.reduce( ( acc: WidgetOptionsGroup, curr: WidgetOptions ): object => {
if ( ! acc[ curr.url ] ) {
acc[ curr.url ] = [];
}
@@ -118,7 +145,7 @@ domReady( () => {
fetch( url )
.then( ( response ) => response.json() )
.then( ( data ) => {
- widgets.forEach( ( widget ) => {
+ widgets.forEach( ( widget: WidgetOptions ) => {
renderWidget( data, widget );
} );
} );
diff --git a/tests/Integration/Blocks/ContentHelperTest.php b/tests/Integration/Blocks/ContentHelperTest.php
index 816b06773..0f76589a6 100644
--- a/tests/Integration/Blocks/ContentHelperTest.php
+++ b/tests/Integration/Blocks/ContentHelperTest.php
@@ -1,6 +1,6 @@
run();
self::assertTrue( wp_script_is( self::BLOCK_NAME ) );
self::assertTrue( wp_style_is( self::BLOCK_NAME ) );
diff --git a/tests/Integration/DashboardLinkTest.php b/tests/Integration/DashboardLinkTest.php
index 71d6e1327..ceaa069bd 100644
--- a/tests/Integration/DashboardLinkTest.php
+++ b/tests/Integration/DashboardLinkTest.php
@@ -39,11 +39,11 @@ public function set_up(): void {
*/
public function test_generate_parsely_post_url(): void {
$post_id = self::factory()->post->create();
- $post = get_post( $post_id );
- $apikey = 'demo-api-key';
+ $post = $this->get_post( $post_id );
+ $site_id = 'demo-site-id';
- $expected = PARSELY::DASHBOARD_BASE_URL . '/demo-api-key/find?url=http%3A%2F%2Fexample.org%2F%3Fp%3D' . $post_id . '&utm_campaign=wp-admin-posts-list&utm_source=wp-admin&utm_medium=wp-parsely';
- $actual = Dashboard_Link::generate_url( $post, $apikey, 'wp-admin-posts-list', 'wp-admin' );
+ $expected = PARSELY::DASHBOARD_BASE_URL . '/demo-site-id/find?url=http%3A%2F%2Fexample.org%2F%3Fp%3D' . $post_id . '&utm_campaign=wp-admin-posts-list&utm_source=wp-admin&utm_medium=wp-parsely';
+ $actual = Dashboard_Link::generate_url( $post, $site_id, 'wp-admin-posts-list', 'wp-admin' );
self::assertSame( $expected, $actual );
}
@@ -60,11 +60,11 @@ public function test_generate_invalid_post_url(): void {
add_filter( 'post_link', '__return_false' );
$post_id = self::factory()->post->create();
- $post = get_post( $post_id );
- $apikey = 'demo-api-key';
+ $post = $this->get_post( $post_id );
+ $site_id = 'demo-site-id';
$expected = '';
- $actual = Dashboard_Link::generate_url( $post, $apikey, 'wp-admin-posts-list', 'wp-admin' );
+ $actual = Dashboard_Link::generate_url( $post, $site_id, 'wp-admin-posts-list', 'wp-admin' );
self::assertSame( $expected, $actual );
}
@@ -76,8 +76,8 @@ public function test_generate_invalid_post_url(): void {
* @since 3.1.0 Moved to `DashboardLinkTest.php`
*
* @covers \Parsely\Dashboard_Link::can_show_link
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::api_key_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\Parsely::update_metadata_endpoint
@@ -99,8 +99,8 @@ public function test_can_correctly_determine_if_Parsely_link_can_be_shown(): voi
* @since 3.1.0 Moved to `DashboardLinkTest.php`
*
* @covers \Parsely\Dashboard_Link::can_show_link
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::api_key_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\Parsely::update_metadata_endpoint
@@ -120,8 +120,8 @@ public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_p
* @since 3.1.0 Moved to `DashboardLinkTest.php`
*
* @covers \Parsely\Dashboard_Link::can_show_link
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::api_key_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\Parsely::update_metadata_endpoint
@@ -142,14 +142,14 @@ public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_p
* @since 3.1.0 Moved to `DashboardLinkTest.php`
*
* @covers \Parsely\Dashboard_Link::can_show_link
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::api_key_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\Parsely::update_metadata_endpoint
* @group ui
*/
- public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_api_key_is_set_or_missing(): void {
+ public function test_can_correctly_determine_if_Parsely_link_can_be_shown_when_site_id_is_set_or_missing(): void {
$published_post = self::factory()->post->create_and_get();
// Site ID is not set.
diff --git a/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php b/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php
index c37b0caab..6935713e6 100644
--- a/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php
+++ b/tests/Integration/Endpoints/AnalyticsPostsProxyEndpointTest.php
@@ -4,8 +4,6 @@
*
* @package Parsely\Tests
* @since 3.5.0
- *
- * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
*/
declare(strict_types=1);
@@ -15,9 +13,12 @@
use Parsely\Endpoints\Analytics_Posts_API_Proxy;
use Parsely\Endpoints\Base_API_Proxy;
use Parsely\Parsely;
-use Parsely\RemoteAPI\Analytics_Posts_Proxy;
+use Parsely\RemoteAPI\Analytics_Posts_API;
+use WP_Error;
use WP_REST_Request;
+use function Parsely\Utils\get_date_format;
+
/**
* Integration Tests for the Analytics Posts API Proxy Endpoint.
*/
@@ -39,7 +40,7 @@ public static function initialize(): void {
public function get_endpoint(): Base_API_Proxy {
return new Analytics_Posts_API_Proxy(
new Parsely(),
- new Analytics_Posts_Proxy( new Parsely() )
+ new Analytics_Posts_API( new Parsely() )
);
}
@@ -49,10 +50,10 @@ public function get_endpoint(): Base_API_Proxy {
* @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
* @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
*/
public function test_register_routes_by_default(): void {
- parent::test_register_routes_by_default();
+ parent::run_test_register_routes_by_default();
}
/**
@@ -62,14 +63,15 @@ public function test_register_routes_by_default(): void {
* @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
* @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
*/
- public function test_do_not_register_route_when_proxy_is_disabled(): void {
- parent::test_do_not_register_route_when_proxy_is_disabled();
+ public function test_verify_that_route_is_not_registered_when_proxy_is_disabled(): void {
+ parent::run_test_do_not_register_route_when_proxy_is_disabled();
}
/**
- * Verifies forbidden error when current user doesn't have proper capabilities.
+ * Verifies forbidden error when current user doesn't have proper
+ * capabilities.
*
* @covers \Parsely\Endpoints\Base_API_Proxy::permission_callback
*
@@ -77,33 +79,115 @@ public function test_do_not_register_route_when_proxy_is_disabled(): void {
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::register_endpoint
*/
public function test_access_of_analytics_posts_endpoint_is_forbidden(): void {
- $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', self::$route ) );
- $error = $response->as_error();
+ $response = rest_get_server()->dispatch(
+ new WP_REST_Request( 'GET', self::$route )
+ );
+ /**
+ * Variable.
+ *
+ * @var WP_Error
+ */
+ $error = $response->as_error();
self::assertSame( 401, $response->get_status() );
self::assertSame( 'rest_forbidden', $error->get_error_code() );
- self::assertSame( 'Sorry, you are not allowed to do that.', $error->get_error_message() );
+ self::assertSame(
+ 'Sorry, you are not allowed to do that.',
+ $error->get_error_message()
+ );
}
/**
* Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an
- * error and does not perform a remote call when the apikey is not populated
+ * error and does not perform a remote call when the Site ID is not populated
* in site options.
*
* @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
* @uses \Parsely\Endpoints\Base_API_Proxy::get_data
* @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
*/
- public function test_get_items_fails_without_apikey_set() {
+ public function test_get_items_fails_when_site_id_is_not_set(): void {
$this->set_admin_user();
- parent::test_get_items_fails_without_apikey_set();
+ parent::run_test_get_items_fails_without_site_id_set();
+ }
+
+ /**
+ * Verifies that calling `GET /wp-parsely/v1/stats/posts` returns an
+ * error and does not perform a remote call when the API Secret is not
+ * populated in site options.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::get_items
+ * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::__construct
+ * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback
+ * @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
+ * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
+ */
+ public function test_get_items_fails_when_api_secret_is_not_set(): void {
+ $this->set_admin_user();
+ parent::run_test_get_items_fails_without_api_secret_set();
+ }
+
+ /**
+ * Verifies default user capability filter.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback
+ *
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call
+ */
+ public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
+ $this->login_as_contributor();
+ add_filter(
+ 'wp_parsely_user_capability_for_all_private_apis',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $proxy_api = new Analytics_Posts_API_Proxy(
+ new Parsely(),
+ new Analytics_Posts_API( new Parsely() )
+ );
+
+ self::assertTrue( $proxy_api->permission_callback() );
+ }
+
+ /**
+ * Verifies endpoint specific user capability filter.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Posts_API_Proxy::permission_callback
+ *
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call
+ */
+ public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void {
+ $this->login_as_contributor();
+ add_filter(
+ 'wp_parsely_user_capability_for_analytics_posts_api',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $proxy_api = new Analytics_Posts_API_Proxy(
+ new Parsely(),
+ new Analytics_Posts_API( new Parsely() )
+ );
+
+ self::assertTrue( $proxy_api->permission_callback() );
}
/**
@@ -117,30 +201,51 @@ public function test_get_items_fails_without_apikey_set() {
* @uses \Parsely\Endpoints\Analytics_Posts_API_Proxy::run
* @uses \Parsely\Endpoints\Base_API_Proxy::get_data
* @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_api_key
+ * @uses \Parsely\Parsely::get_site_id
* @uses \Parsely\Parsely::get_api_secret
* @uses \Parsely\Parsely::get_options
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
- * @uses \Parsely\RemoteAPI\Base_Proxy::get_api_url
- * @uses \Parsely\RemoteAPI\Base_Proxy::get_items
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items
*/
- public function test_get_items() {
- TestCase::set_options( array( 'apikey' => 'example.com' ) );
- TestCase::set_options( array( 'api_secret' => 'test' ) );
+ public function test_get_items(): void {
$this->set_admin_user();
+ TestCase::set_options(
+ array(
+ 'apikey' => 'example.com',
+ 'api_secret' => 'test',
+ )
+ );
$dispatched = 0;
- $date_format = get_option( 'date_format' );
+ $date_format = get_date_format();
add_filter(
'pre_http_request',
function () use ( &$dispatched ) {
$dispatched++;
return array(
- 'body' => '{"data":[{"_hits": 142, "author": "Aakash Shah", "authors": ["Aakash Shah"], "full_content_word_count": 3624, "image_url": "https://blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png?w=150&h=150&crop=1", "link": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api", "metadata": "", "metrics": {"views": 142}, "pub_date": "2020-04-06T13:30:58", "section": "Analytics That Matter", "tags": ["animalz", "parsely_smart:entity:Bounce rate", "parsely_smart:entity:Customer analytics", "parsely_smart:entity:Digital marketing", "parsely_smart:entity:Google Analytics", "parsely_smart:entity:Marketing strategy", "parsely_smart:entity:Multivariate testing in marketing", "parsely_smart:entity:Open source", "parsely_smart:entity:Pageview", "parsely_smart:entity:Search engine optimization", "parsely_smart:entity:Social media", "parsely_smart:entity:Social media analytics", "parsely_smart:entity:Usability", "parsely_smart:entity:User experience design", "parsely_smart:entity:Web analytics", "parsely_smart:entity:Web traffic", "parsely_smart:entity:Website", "parsely_smart:entity:World Wide Web", "parsely_smart:iab:Business", "parsely_smart:iab:Graphics", "parsely_smart:iab:Software", "parsely_smart:iab:Technology"], "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1", "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need", "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api"}, {"_hits": 40, "author": "Stephanie Schwartz and Andrew Butler", "authors": ["Stephanie Schwartz and Andrew Butler"], "full_content_word_count": 1785, "image_url": "https://blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg?w=150&h=150&crop=1", "link": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api", "metadata": "", "metrics": {"views": 40}, "pub_date": "2021-04-30T20:30:24", "section": "Analytics That Matter", "tags": ["parsely_smart:entity:Analytics", "parsely_smart:entity:Best practice", "parsely_smart:entity:Hashtag", "parsely_smart:entity:Metadata", "parsely_smart:entity:Search engine", "parsely_smart:entity:Search engine optimization", "parsely_smart:entity:Tag (metadata)", "parsely_smart:iab:Business", "parsely_smart:iab:Science", "parsely_smart:iab:Software", "parsely_smart:iab:Technology"], "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1", "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy", "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api"}]}',
+ 'body' => '{"data":[
+ {
+ "author": "Aakash Shah",
+ "metrics": {"views": 142},
+ "pub_date": "2020-04-06T13:30:58",
+ "thumb_url_medium": "https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1",
+ "title": "9 Types of Web Analytics Tools \u2014 And How to Know Which Ones You Really Need",
+ "url": "https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api"
+ },
+ {
+ "author": "Stephanie Schwartz and Andrew Butler",
+ "metrics": {"views": 40},
+ "pub_date": "2021-04-30T20:30:24",
+ "thumb_url_medium": "https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1",
+ "title": "5 Tagging Best Practices For Getting the Most Out of Your Content Strategy",
+ "url": "https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api"
+ }
+ ]}',
);
}
);
@@ -153,22 +258,26 @@ function () use ( &$dispatched ) {
(object) array(
'data' => array(
(object) array(
- 'author' => 'Aakash Shah',
- 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ),
- 'id' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api',
- 'statsUrl' => PARSELY::DASHBOARD_BASE_URL . '/blog.parsely.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F%3Fitm_source%3Dparsely-api',
- 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need',
- 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api',
- 'views' => 142,
+ 'author' => 'Aakash Shah',
+ 'date' => wp_date( $date_format, strtotime( '2020-04-06T13:30:58' ) ),
+ 'id' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api',
+ 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2Fweb-analytics-software-tools%2F%3Fitm_source%3Dparsely-api',
+ 'thumbUrlMedium' => 'https://images.parsely.com/XCmTXuOf8yVbUYTxj2abQ4RSDkM=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/06/Web-Analytics-Tool.png%3Fw%3D150%26h%3D150%26crop%3D1',
+ 'title' => '9 Types of Web Analytics Tools — And How to Know Which Ones You Really Need',
+ 'url' => 'https://blog.parse.ly/web-analytics-software-tools/?itm_source=parsely-api',
+ 'views' => 142,
+ 'postId' => 0,
),
(object) array(
- 'author' => 'Stephanie Schwartz and Andrew Butler',
- 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ),
- 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api',
- 'statsUrl' => PARSELY::DASHBOARD_BASE_URL . '/blog.parsely.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F%3Fitm_source%3Dparsely-api',
- 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy',
- 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api',
- 'views' => 40,
+ 'author' => 'Stephanie Schwartz and Andrew Butler',
+ 'date' => wp_date( $date_format, strtotime( '2021-04-30T20:30:24' ) ),
+ 'id' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api',
+ 'dashUrl' => PARSELY::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fblog.parse.ly%2F5-tagging-best-practices-content-strategy%2F%3Fitm_source%3Dparsely-api',
+ 'thumbUrlMedium' => 'https://images.parsely.com/ap3YSufqxnLpz6zzQshoks3snXI=/85x85/smart/https%3A//blog.parse.ly/wp-content/uploads/2021/05/pexels-brett-jordan-998501-1024x768-2.jpeg%3Fw%3D150%26h%3D150%26crop%3D1',
+ 'title' => '5 Tagging Best Practices For Getting the Most Out of Your Content Strategy',
+ 'url' => 'https://blog.parse.ly/5-tagging-best-practices-content-strategy/?itm_source=parsely-api',
+ 'views' => 40,
+ 'postId' => 0,
),
),
),
diff --git a/tests/Integration/Endpoints/GraphQLMetadataTest.php b/tests/Integration/Endpoints/GraphQLMetadataTest.php
index 1cc26ff4e..1d53747f9 100644
--- a/tests/Integration/Endpoints/GraphQLMetadataTest.php
+++ b/tests/Integration/Endpoints/GraphQLMetadataTest.php
@@ -49,7 +49,7 @@ public function set_up(): void {
*
* @covers \Parsely\Endpoints\GraphQL_Metadata::run
* @uses \Parsely\Endpoints\Metadata_Endpoint::__construct
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
public function test_graphql_enqueued(): void {
@@ -76,16 +76,16 @@ public function test_graphql_enqueued_filter(): void {
}
/**
- * Verifies that GraphQL types are not registered if there's no API key.
+ * Verifies that GraphQL types are not registered if there's no Site ID.
*
* @since 3.2.0
*
* @covers \Parsely\Endpoints\GraphQL_Metadata::run
* @uses \Parsely\Endpoints\Metadata_Endpoint::__construct
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
- public function test_graphql_enqueued_no_api_key(): void {
+ public function test_graphql_enqueued_no_site_id(): void {
self::$graphql->run();
self::assertFalse( has_filter( 'graphql_register_types', array( self::$graphql, 'register_meta' ) ) );
}
diff --git a/tests/Integration/Endpoints/ReferrersPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/ReferrersPostDetailProxyEndpointTest.php
new file mode 100644
index 000000000..b78337b4b
--- /dev/null
+++ b/tests/Integration/Endpoints/ReferrersPostDetailProxyEndpointTest.php
@@ -0,0 +1,331 @@
+set_admin_user();
+ parent::run_test_get_items_fails_without_site_id_set();
+ }
+
+ /**
+ * Verifies that calling `GET /wp-parsely/v1/referrers/post/detail` returns
+ * an error and does not perform a remote call when the Site ID is not
+ * populated in site options.
+ *
+ * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items
+ * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
+ * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ */
+ public function test_get_items_fails_when_api_secret_is_not_set(): void {
+ $this->set_admin_user();
+ parent::run_test_get_items_fails_without_api_secret_set();
+ }
+
+ /**
+ * Verifies default user capability filter.
+ *
+ * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback
+ *
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call
+ */
+ public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
+ $this->login_as_contributor();
+ add_filter(
+ 'wp_parsely_user_capability_for_all_private_apis',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $proxy_api = new Referrers_Post_Detail_API_Proxy(
+ new Parsely(),
+ new Referrers_Post_Detail_API( new Parsely() )
+ );
+
+ self::assertTrue( $proxy_api->permission_callback() );
+ }
+
+ /**
+ * Verifies endpoint specific user capability filter.
+ *
+ * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback
+ *
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call
+ */
+ public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void {
+ $this->login_as_contributor();
+ add_filter(
+ 'wp_parsely_user_capability_for_referrers_post_detail_api',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $proxy_api = new Referrers_Post_Detail_API_Proxy(
+ new Parsely(),
+ new Referrers_Post_Detail_API( new Parsely() )
+ );
+
+ self::assertTrue( $proxy_api->permission_callback() );
+ }
+
+ /**
+ * Verifies that calls to `GET /wp-parsely/v1/referrers/post/detail` return
+ * results in the expected format.
+ *
+ * @covers \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::get_items
+ * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
+ * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::__construct
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::generate_data
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::permission_callback
+ * @uses \Parsely\Endpoints\Referrers_Post_Detail_API_Proxy::run
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_site_id
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items
+ */
+ public function test_get_items(): void {
+ $this->set_admin_user();
+ TestCase::set_options(
+ array(
+ 'apikey' => 'example.com',
+ 'api_secret' => 'test',
+ )
+ );
+
+ $dispatched = 0;
+
+ add_filter(
+ 'pre_http_request',
+ function () use ( &$dispatched ) {
+ $dispatched++;
+ return array(
+ 'body' => '{"data":[
+ {
+ "metrics": {"referrers_views": 1500},
+ "name": "google",
+ "type": "search"
+ },
+ {
+ "metrics": {"referrers_views": 100},
+ "name": "blog.parse.ly",
+ "type": "internal"
+ },
+ {
+ "metrics": {"referrers_views": 50},
+ "name": "bing",
+ "type": "search"
+ },
+ {
+ "metrics": {"referrers_views": 30},
+ "name": "facebook.com",
+ "type": "social"
+ },
+ {
+ "metrics": {"referrers_views": 10},
+ "name": "okt.to",
+ "type": "other"
+ },
+ {
+ "metrics": {"referrers_views": 10},
+ "name": "yandex",
+ "type": "search"
+ },
+ {
+ "metrics": {"referrers_views": 10},
+ "name": "parse.ly",
+ "type": "internal"
+ },
+ {
+ "metrics": {"referrers_views": 10},
+ "name": "yahoo!",
+ "type": "search"
+ },
+ {
+ "metrics": {"referrers_views": 5},
+ "name": "site1.com",
+ "type": "other"
+ },
+ {
+ "metrics": {"referrers_views": 5},
+ "name": "link.site2.com",
+ "type": "other"
+ }
+ ]}',
+ );
+ }
+ );
+
+ $expected_top = (object) array(
+ 'direct' => (object) array(
+ 'views' => '770',
+ 'viewsPercentage' => '30.80',
+ 'datasetViewsPercentage' => '31.43',
+ ),
+ 'google' => (object) array(
+ 'views' => '1,500',
+ 'viewsPercentage' => '60.00',
+ 'datasetViewsPercentage' => '61.22',
+ ),
+ 'blog.parse.ly' => (object) array(
+ 'views' => '100',
+ 'viewsPercentage' => '4.00',
+ 'datasetViewsPercentage' => '4.08',
+ ),
+ 'bing' => (object) array(
+ 'views' => '50',
+ 'viewsPercentage' => '2.00',
+ 'datasetViewsPercentage' => '2.04',
+ ),
+ 'facebook.com' => (object) array(
+ 'views' => '30',
+ 'viewsPercentage' => '1.20',
+ 'datasetViewsPercentage' => '1.22',
+ ),
+ 'totals' => (object) array(
+ 'views' => '2,450',
+ 'viewsPercentage' => '98.00',
+ 'datasetViewsPercentage' => '100.00',
+ ),
+ );
+
+ $expected_types = (object) array(
+ 'social' => (object) array(
+ 'views' => '30',
+ 'viewsPercentage' => '1.20',
+ ),
+ 'search' => (object) array(
+ 'views' => '1,570',
+ 'viewsPercentage' => '62.80',
+ ),
+ 'other' => (object) array(
+ 'views' => '20',
+ 'viewsPercentage' => '0.80',
+ ),
+ 'internal' => (object) array(
+ 'views' => '110',
+ 'viewsPercentage' => '4.40',
+ ),
+ 'direct' => (object) array(
+ 'views' => '770',
+ 'viewsPercentage' => '30.80',
+ ),
+ 'totals' => (object) array(
+ 'views' => '2,500',
+ 'viewsPercentage' => '100.00',
+ ),
+ );
+
+ $request = new WP_REST_Request( 'GET', self::$route );
+ $request->set_param( 'total_views', '2,500' );
+
+ $response = rest_get_server()->dispatch( $request );
+
+ self::assertSame( 1, $dispatched );
+ self::assertSame( 200, $response->get_status() );
+ self::assertEquals(
+ (object) array(
+ 'data' => array(
+ 'top' => $expected_top,
+ 'types' => $expected_types,
+ ),
+ ),
+ $response->get_data()
+ );
+ }
+}
diff --git a/tests/Integration/Endpoints/RelatedProxyEndpointTest.php b/tests/Integration/Endpoints/RelatedProxyEndpointTest.php
index b81303d8c..09cc116ac 100644
--- a/tests/Integration/Endpoints/RelatedProxyEndpointTest.php
+++ b/tests/Integration/Endpoints/RelatedProxyEndpointTest.php
@@ -3,8 +3,6 @@
* Integration Tests: Related API Proxy Endpoint
*
* @package Parsely\Tests
- *
- * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
*/
declare(strict_types=1);
@@ -14,7 +12,7 @@
use Parsely\Endpoints\Base_API_Proxy;
use Parsely\Endpoints\Related_API_Proxy;
use Parsely\Parsely;
-use Parsely\RemoteAPI\Related_Proxy;
+use Parsely\RemoteAPI\Related_API;
use WP_REST_Request;
/**
@@ -38,7 +36,7 @@ public static function initialize(): void {
public function get_endpoint(): Base_API_Proxy {
return new Related_API_Proxy(
new Parsely(),
- new Related_Proxy( new Parsely() )
+ new Related_API( new Parsely() )
);
}
@@ -48,10 +46,10 @@ public function get_endpoint(): Base_API_Proxy {
* @covers \Parsely\Endpoints\Related_API_Proxy::run
* @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
* @uses \Parsely\Endpoints\Related_API_Proxy::__construct
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
*/
public function test_register_routes_by_default(): void {
- parent::test_register_routes_by_default();
+ parent::run_test_register_routes_by_default();
}
/**
@@ -61,15 +59,15 @@ public function test_register_routes_by_default(): void {
* @covers \Parsely\Endpoints\Related_API_Proxy::run
* @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
* @uses \Parsely\Endpoints\Related_API_Proxy::__construct
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
*/
- public function test_do_not_register_route_when_proxy_is_disabled(): void {
- parent::test_do_not_register_route_when_proxy_is_disabled();
+ public function test_verify_that_route_is_not_registered_when_proxy_is_disabled(): void {
+ parent::run_test_do_not_register_route_when_proxy_is_disabled();
}
/**
* Verifies that calling `GET /wp-parsely/v1/related` returns an error and
- * does not perform a remote call when the apikey is not populated
+ * does not perform a remote call when the Site ID is not populated
* in site options.
*
* @covers \Parsely\Endpoints\Related_API_Proxy::get_items
@@ -78,13 +76,13 @@ public function test_do_not_register_route_when_proxy_is_disabled(): void {
* @uses \Parsely\Endpoints\Related_API_Proxy::__construct
* @uses \Parsely\Endpoints\Related_API_Proxy::permission_callback
* @uses \Parsely\Endpoints\Related_API_Proxy::run
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
*/
- public function test_get_items_fails_without_apikey_set() {
- parent::test_get_items_fails_without_apikey_set();
+ public function test_get_items_fails_when_site_id_is_not_set(): void {
+ parent::run_test_get_items_fails_without_site_id_set();
}
/**
@@ -98,16 +96,16 @@ public function test_get_items_fails_without_apikey_set() {
* @uses \Parsely\Endpoints\Related_API_Proxy::generate_data
* @uses \Parsely\Endpoints\Related_API_Proxy::permission_callback
* @uses \Parsely\Endpoints\Related_API_Proxy::run
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::api_secret_is_set
- * @uses \Parsely\Parsely::get_api_key
+ * @uses \Parsely\Parsely::get_site_id
* @uses \Parsely\Parsely::get_options
- * @uses \Parsely\RemoteAPI\Base_Proxy::__construct
- * @uses \Parsely\RemoteAPI\Base_Proxy::get_api_url
- * @uses \Parsely\RemoteAPI\Base_Proxy::get_items
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items
*/
- public function test_get_items() {
+ public function test_get_items(): void {
TestCase::set_options( array( 'apikey' => 'example.com' ) );
$dispatched = 0;
@@ -117,7 +115,20 @@ public function test_get_items() {
function () use ( &$dispatched ) {
$dispatched++;
return array(
- 'body' => '{"data":[{"image_url":"https:\/\/example.com\/img.png","thumb_url_medium":"https:\/\/example.com\/thumb.png","title":"something","url":"https:\/\/example.com"},{"image_url":"https:\/\/example.com\/img2.png","thumb_url_medium":"https:\/\/example.com\/thumb2.png","title":"something2","url":"https:\/\/example.com\/2"}]}',
+ 'body' => '{"data":[
+ {
+ "image_url":"https:\/\/example.com\/img.png",
+ "thumb_url_medium":"https:\/\/example.com\/thumb.png",
+ "title":"something",
+ "url":"https:\/\/example.com"
+ },
+ {
+ "image_url":"https:\/\/example.com\/img2.png",
+ "thumb_url_medium":"https:\/\/example.com\/thumb2.png",
+ "title":"something2",
+ "url":"https:\/\/example.com\/2"
+ }
+ ]}',
);
}
);
diff --git a/tests/Integration/Endpoints/RestMetadataTest.php b/tests/Integration/Endpoints/RestMetadataTest.php
index 6785cbe64..d360eec0a 100644
--- a/tests/Integration/Endpoints/RestMetadataTest.php
+++ b/tests/Integration/Endpoints/RestMetadataTest.php
@@ -50,7 +50,7 @@ public function set_up(): void {
* @covers \Parsely\Endpoints\Rest_Metadata::run
* @uses \Parsely\Endpoints\Rest_Metadata::register_meta
* @uses \Parsely\Endpoints\Metadata_Endpoint::__construct
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
public function test_register_enqueued_rest_init(): void {
@@ -85,14 +85,14 @@ public function test_register_enqueued_rest_init_filter(): void {
/**
* Verifies that the logic has not been enqueued when the `run` method is
- * called with no API key.
+ * called with no Site ID.
*
* @covers \Parsely\Endpoints\Rest_Metadata::run
* @uses \Parsely\Endpoints\Metadata_Endpoint::__construct
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
- public function test_register_enqueued_rest_init_no_api_key(): void {
+ public function test_register_enqueued_rest_init_no_site_id(): void {
global $wp_rest_additional_fields;
self::$rest->run();
@@ -105,7 +105,7 @@ public function test_register_enqueued_rest_init_no_api_key(): void {
*
* @covers \Parsely\Endpoints\Rest_Metadata::register_meta
* @uses \Parsely\Endpoints\Metadata_Endpoint::__construct
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
public function test_register_meta_registers_fields(): void {
@@ -182,9 +182,9 @@ function() {
* @uses \Parsely\Metadata\Post_Builder::get_coauthor_names
* @uses \Parsely\Metadata\Post_Builder::get_metadata
* @uses \Parsely\Metadata\Post_Builder::get_tags
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::get_api_key
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::get_site_id
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::get_tracker_url
* @uses \Parsely\Parsely::post_has_trackable_status
@@ -196,13 +196,13 @@ public function test_get_callback(): void {
$post_id = self::factory()->post->create();
// Go to current post to update WP_Query with correct data.
- $this->go_to( get_permalink( $post_id ) );
+ $this->go_to( $this->get_permalink( $post_id ) );
- $meta_object = self::$rest->get_callback( get_post( $post_id, 'ARRAY_A' ) );
+ $meta_object = self::$rest->get_callback( $this->get_post_in_array( $post_id ) );
$metadata = new Metadata( self::$parsely );
$expected = array(
'version' => '1.1.0',
- 'meta' => $metadata->construct_metadata( get_post( $post_id ) ),
+ 'meta' => $metadata->construct_metadata( $this->get_post( $post_id ) ),
'rendered' => self::$rest->get_rendered_meta( 'json_ld' ),
'tracker_url' => 'https://cdn.parsely.com/keys/testkey/p.js',
);
@@ -241,9 +241,9 @@ public function test_get_callback(): void {
* @uses \Parsely\Metadata\Post_Builder::get_coauthor_names
* @uses \Parsely\Metadata\Post_Builder::get_metadata
* @uses \Parsely\Metadata\Post_Builder::get_tags
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::get_api_key
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::get_site_id
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::get_tracker_url
* @uses \Parsely\Parsely::post_has_trackable_status
@@ -253,11 +253,11 @@ public function test_get_callback_with_filter(): void {
self::set_options( array( 'apikey' => 'testkey' ) );
$post_id = self::factory()->post->create();
- $meta_object = self::$rest->get_callback( get_post( $post_id, 'ARRAY_A' ) );
+ $meta_object = self::$rest->get_callback( $this->get_post_in_array( $post_id ) );
$metadata = new Metadata( self::$parsely );
$expected = array(
'version' => '1.1.0',
- 'meta' => $metadata->construct_metadata( get_post( $post_id ) ),
+ 'meta' => $metadata->construct_metadata( $this->get_post( $post_id ) ),
'tracker_url' => 'https://cdn.parsely.com/keys/testkey/p.js',
);
@@ -295,8 +295,8 @@ public function test_get_callback_with_filter(): void {
* @uses \Parsely\Metadata\Post_Builder::get_coauthor_names
* @uses \Parsely\Metadata\Post_Builder::get_metadata
* @uses \Parsely\Metadata\Post_Builder::get_tags
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\UI\Metadata_Renderer::__construct
@@ -308,13 +308,13 @@ public function test_get_callback_with_url_filter(): void {
$post_id = self::factory()->post->create();
// Go to current post to update WP_Query with correct data.
- $this->go_to( get_permalink( $post_id ) );
+ $this->go_to( $this->get_permalink( $post_id ) );
- $meta_object = self::$rest->get_callback( get_post( $post_id, 'ARRAY_A' ) );
+ $meta_object = self::$rest->get_callback( $this->get_post_in_array( $post_id ) );
$metadata = new Metadata( self::$parsely );
$expected = array(
'version' => '1.1.0',
- 'meta' => $metadata->construct_metadata( get_post( $post_id ) ),
+ 'meta' => $metadata->construct_metadata( $this->get_post( $post_id ) ),
'rendered' => self::$rest->get_rendered_meta( 'json_ld' ),
);
@@ -328,8 +328,8 @@ public function test_get_callback_with_url_filter(): void {
* @covers \Parsely\Endpoints\Rest_Metadata::get_callback
* @uses \Parsely\Endpoints\Metadata_Endpoint::__construct
* @uses \Parsely\Endpoints\Metadata_Endpoint::get_rendered_meta
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::get_tracker_url
* @uses \Parsely\UI\Metadata_Renderer::__construct
@@ -378,8 +378,8 @@ public function test_get_callback_with_non_existent_post(): void {
* @uses \Parsely\Metadata\Post_Builder::get_coauthor_names
* @uses \Parsely\Metadata\Post_Builder::get_metadata
* @uses \Parsely\Metadata\Post_Builder::get_tags
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\UI\Metadata_Renderer::__construct
@@ -396,11 +396,11 @@ public function test_get_rendered_meta_json_ld(): void {
);
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
- $post = get_post( $post_id );
- $date = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
+ $post = $this->get_post( $post_id );
+ $date = gmdate( 'Y-m-d\TH:i:s\Z', $this->get_post_time_in_int( 'U', true, $post ) );
// Go to current post to update WP_Query with correct data.
- $this->go_to( get_permalink( $post_id ) );
+ $this->go_to( $this->get_permalink( $post_id ) );
$meta_string = self::$rest->get_rendered_meta( 'json_ld' );
$expected = '';
@@ -438,8 +438,8 @@ public function test_get_rendered_meta_json_ld(): void {
* @uses \Parsely\Metadata\Post_Builder::get_coauthor_names
* @uses \Parsely\Metadata\Post_Builder::get_metadata
* @uses \Parsely\Metadata\Post_Builder::get_tags
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
* @uses \Parsely\Parsely::convert_jsonld_to_parsely_type
@@ -459,11 +459,11 @@ public function test_get_rendered_repeated_metas(): void {
);
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
- $post = get_post( $post_id );
- $date = gmdate( 'Y-m-d\TH:i:s\Z', get_post_time( 'U', true, $post ) );
+ $post = $this->get_post( $post_id );
+ $date = gmdate( 'Y-m-d\TH:i:s\Z', $this->get_post_time_in_int( 'U', true, $post ) );
// Go to current post to update WP_Query with correct data.
- $this->go_to( get_permalink( $post_id ) );
+ $this->go_to( $this->get_permalink( $post_id ) );
$meta_string = self::$rest->get_rendered_meta( 'repeated_metas' );
$expected = '
@@ -481,6 +481,8 @@ public function test_get_rendered_repeated_metas(): void {
*
* @param string $post_type Post type.
* @param array $wp_rest_additional_fields Global variable.
+ *
+ * @phpstan-ignore-next-line
*/
private function assertParselyRestFieldIsConstructedCorrectly( string $post_type, array $wp_rest_additional_fields ): void {
self::assertArrayHasKey( $post_type, $wp_rest_additional_fields );
diff --git a/tests/Integration/Endpoints/StatsPostDetailProxyEndpointTest.php b/tests/Integration/Endpoints/StatsPostDetailProxyEndpointTest.php
new file mode 100644
index 000000000..ac761c7d2
--- /dev/null
+++ b/tests/Integration/Endpoints/StatsPostDetailProxyEndpointTest.php
@@ -0,0 +1,231 @@
+set_admin_user();
+ parent::run_test_get_items_fails_without_site_id_set();
+ }
+
+ /**
+ * Verifies that calling `GET /wp-parsely/v1/analytics/post/detail` returns
+ * an error and does not perform a remote call when the Site ID is not
+ * populated in site options.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items
+ * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
+ * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ */
+ public function test_get_items_fails_when_api_secret_is_not_set(): void {
+ $this->set_admin_user();
+ parent::run_test_get_items_fails_without_api_secret_set();
+ }
+
+ /**
+ * Verifies default user capability filter.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback
+ *
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call
+ */
+ public function test_user_is_allowed_to_make_proxy_api_call_if_default_user_capability_is_changed(): void {
+ $this->login_as_contributor();
+ add_filter(
+ 'wp_parsely_user_capability_for_all_private_apis',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $proxy_api = new Analytics_Post_Detail_API_Proxy(
+ new Parsely(),
+ new Analytics_Post_Detail_API( new Parsely() )
+ );
+
+ self::assertTrue( $proxy_api->permission_callback() );
+ }
+
+ /**
+ * Verifies endpoint specific user capability filter.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback
+ *
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::is_user_allowed_to_make_api_call
+ */
+ public function test_user_is_allowed_to_make_proxy_api_call_if_endpoint_specific_user_capability_is_changed(): void {
+ $this->login_as_contributor();
+ add_filter(
+ 'wp_parsely_user_capability_for_analytics_post_detail_api',
+ function () {
+ return 'edit_posts';
+ }
+ );
+
+ $proxy_api = new Analytics_Post_Detail_API_Proxy(
+ new Parsely(),
+ new Analytics_Post_Detail_API( new Parsely() )
+ );
+
+ self::assertTrue( $proxy_api->permission_callback() );
+ }
+
+ /**
+ * Verifies that calls to `GET /wp-parsely/v1/analytics/post/detail` return
+ * results in the expected format.
+ *
+ * @covers \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::get_items
+ * @uses \Parsely\Endpoints\Base_API_Proxy::get_data
+ * @uses \Parsely\Endpoints\Base_API_Proxy::register_endpoint
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::__construct
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::generate_data
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::permission_callback
+ * @uses \Parsely\Endpoints\Analytics_Post_Detail_API_Proxy::run
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::api_secret_is_set
+ * @uses \Parsely\Parsely::get_site_id
+ * @uses \Parsely\Parsely::get_options
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::__construct
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_api_url
+ * @uses \Parsely\RemoteAPI\Remote_API_Base::get_items
+ */
+ public function test_get_items(): void {
+ $this->set_admin_user();
+ TestCase::set_options(
+ array(
+ 'apikey' => 'example.com',
+ 'api_secret' => 'test',
+ )
+ );
+
+ $dispatched = 0;
+
+ add_filter(
+ 'pre_http_request',
+ function () use ( &$dispatched ) {
+ $dispatched++;
+ return array(
+ 'body' => '
+ {"data":[{
+ "avg_engaged": 1.911,
+ "metrics": {
+ "views": 2158,
+ "visitors": 1537
+ },
+ "url": "https://example.com"
+ }]}
+ ',
+ );
+ }
+ );
+
+ $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/wp-parsely/v1/stats/post/detail' ) );
+
+ self::assertSame( 1, $dispatched );
+ self::assertSame( 200, $response->get_status() );
+ self::assertEquals(
+ (object) array(
+ 'data' => array(
+ (object) array(
+ 'avgEngaged' => ' 1:55',
+ 'dashUrl' => Parsely::DASHBOARD_BASE_URL . '/example.com/find?url=https%3A%2F%2Fexample.com',
+ 'url' => 'https://example.com',
+ 'views' => '2,158',
+ 'visitors' => '1,537',
+ ),
+ ),
+ ),
+ $response->get_data()
+ );
+ }
+}
diff --git a/tests/Integration/Integrations/AmpTest.php b/tests/Integration/Integrations/AmpTest.php
index 7ff1c5992..f695b65da 100644
--- a/tests/Integration/Integrations/AmpTest.php
+++ b/tests/Integration/Integrations/AmpTest.php
@@ -15,6 +15,9 @@
/**
* Integration Tests for the AMP Integration.
+ *
+ * @phpstan-import-type Amp_Analytics from Amp
+ * @phpstan-import-type Amp_Native_Analytics from Amp
*/
final class AmpTest extends TestCase {
/**
@@ -122,16 +125,21 @@ public function test_can_register_Parsely_for_AMP_analytics(): void {
$amp = new Amp( self::$parsely );
$analytics = array();
- // If apikey is empty, $analytics are returned.
+ // If Site ID is empty, $analytics are returned.
self::assertSame( $analytics, $amp->register_parsely_for_amp_analytics( $analytics ) );
// Now set the key and test for changes.
- self::set_options( array( 'apikey' => 'my-api-key.com' ) );
+ self::set_options( array( 'apikey' => 'my-site-id.com' ) );
+ /**
+ * Variable.
+ *
+ * @var Amp_Analytics
+ */
$output = $amp->register_parsely_for_amp_analytics( $analytics );
self::assertSame( 'parsely', $output['parsely']['type'] );
- self::assertSame( 'my-api-key.com', $output['parsely']['config_data']['vars']['apikey'] );
+ self::assertSame( 'my-site-id.com', $output['parsely']['config_data']['vars']['apikey'] );
}
/**
@@ -149,7 +157,7 @@ public function test_can_register_Parsely_for_AMP_native_analytics(): void {
$amp = new Amp( self::$parsely );
$analytics = array();
- // If apikey is empty, $analytics are returned.
+ // If Site ID is empty, $analytics are returned.
self::assertSame( $analytics, $amp->register_parsely_for_amp_native_analytics( $analytics ) );
// Check with AMP marked as disabled.
@@ -157,16 +165,21 @@ public function test_can_register_Parsely_for_AMP_native_analytics(): void {
self::assertSame( $analytics, $amp->register_parsely_for_amp_native_analytics( $analytics ) );
- // Now enable AMP, and set the API key and test for changes.
+ // Now enable AMP, and set the Site ID and test for changes.
self::set_options(
array(
'disable_amp' => false,
- 'apikey' => 'my-api-key.com',
+ 'apikey' => 'my-site-id.com',
)
);
+ /**
+ * Variable.
+ *
+ * @var Amp_Native_Analytics
+ */
$output = $amp->register_parsely_for_amp_native_analytics( $analytics );
self::assertSame( 'parsely', $output['parsely']['type'] );
- self::assertStringContainsString( 'my-api-key.com', $output['parsely']['config'] );
+ self::assertStringContainsString( 'my-site-id.com', $output['parsely']['config'] );
}
}
diff --git a/tests/Integration/Integrations/FacebookInstantArticlesTest.php b/tests/Integration/Integrations/FacebookInstantArticlesTest.php
index e41a9e972..9d1394238 100644
--- a/tests/Integration/Integrations/FacebookInstantArticlesTest.php
+++ b/tests/Integration/Integrations/FacebookInstantArticlesTest.php
@@ -16,6 +16,9 @@
/**
* Integration Tests for the Facebook Instant Articles Integration.
+ *
+ * @phpstan-import-type FB_Instant_Articles_Registry from Facebook_Instant_Articles
+ * @phpstan-import-type FB_Parsely_Registry from Facebook_Instant_Articles
*/
final class FacebookInstantArticlesTest extends TestCase {
/**
@@ -48,8 +51,15 @@ public function set_up(): void {
self::$fbia = new Facebook_Instant_Articles( new Parsely() );
$reflect = new ReflectionClass( self::$fbia );
- self::$registry_identifier = $reflect->getReflectionConstant( 'REGISTRY_IDENTIFIER' )->getValue();
- self::$registry_display_name = $reflect->getReflectionConstant( 'REGISTRY_DISPLAY_NAME' )->getValue();
+ $registry_identifier = $reflect->getReflectionConstant( 'REGISTRY_IDENTIFIER' );
+ if ( false !== $registry_identifier ) {
+ self::$registry_identifier = $registry_identifier->getValue(); // @phpstan-ignore-line
+ }
+
+ $registry_display_name = $reflect->getReflectionConstant( 'REGISTRY_DISPLAY_NAME' );
+ if ( false !== $registry_display_name ) {
+ self::$registry_display_name = $registry_display_name->getValue(); // @phpstan-ignore-line
+ }
}
/**
@@ -81,19 +91,22 @@ public function test_integration_only_runs_when_FBIA_plugin_is_active(): void {
}
/**
- * Verifies that the integration is active only if an API key is set.
+ * Verifies that the integration is active only if a Site ID is set.
*
* @covers \Parsely\Integrations\Facebook_Instant_Articles::insert_parsely_tracking
* @covers \Parsely\Integrations\Facebook_Instant_Articles::get_embed_code
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
- * @uses \Parsely\Parsely::get_api_key
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
+ * @uses \Parsely\Parsely::get_site_id
* @uses \Parsely\Parsely::get_options
* @group fbia
*/
public function test_parsely_is_added_to_FBIA_registry(): void {
- // We use our own registry here, but the integration with the FBIA
- // plugin provides its own.
+ /**
+ * We use our own registry here, but the integration with the FBIA plugin provides its own.
+ *
+ * @var FB_Instant_Articles_Registry
+ */
$registry = array();
// Site ID is not set.
@@ -101,25 +114,32 @@ public function test_parsely_is_added_to_FBIA_registry(): void {
self::assertArrayNotHasKey( self::$registry_identifier, $registry );
// Site ID is set.
- $fake_api_key = 'my-api-key.com';
- self::set_options( array( 'apikey' => $fake_api_key ) );
+ $fake_site_id = 'my-site-id.com';
+ self::set_options( array( 'apikey' => $fake_site_id ) );
self::$fbia->insert_parsely_tracking( $registry );
- self::assert_parsely_added_to_registry( $registry, $fake_api_key );
+ self::assert_parsely_added_to_registry( $registry, $fake_site_id );
}
/**
* Verifies that the registry array has the integration identifier as a key,
* and that the display name and payload are correct.
*
- * @param array $registry Representation of Facebook Instant Articles registry.
- * @param string $api_key API key.
+ * @param FB_Instant_Articles_Registry $registry Representation of Facebook Instant Articles registry.
+ * @param string $site_id Site ID.
*/
- public static function assert_parsely_added_to_registry( array $registry, string $api_key ): void {
+ public static function assert_parsely_added_to_registry( $registry, string $site_id ): void {
self::assertArrayHasKey( self::$registry_identifier, $registry );
- self::assertSame( self::$registry_display_name, $registry[ self::$registry_identifier ]['name'] );
- // Payload should contain a script tag and the API key.
- self::assertStringContainsString( '", $output );
+ self::assertStringContainsString( "", $output );
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
self::assertStringContainsString( "", $output );
}
-
- /**
- * Asserts that a passed script is not registered.
- *
- * @param string $handle Script handle to test.
- */
- private function assert_is_script_not_registered( string $handle ): void {
- $this->assert_script_statuses( $handle, array(), array( 'registered' ) );
- }
-
- /**
- * Asserts that a passed script is registered.
- *
- * @param string $handle Script handle to test.
- */
- private function assert_is_script_registered( string $handle ): void {
- $this->assert_script_statuses( $handle, array( 'registered' ) );
- }
-
- /**
- * Asserts that a passed script is not enqueued.
- *
- * @param string $handle Script handle to test.
- */
- private function assert_is_script_not_enqueued( string $handle ): void {
- $this->assert_script_statuses( $handle, array(), array( 'enqueued' ) );
- }
-
- /**
- * Asserts that a passed script is enqueued.
- *
- * @param string $handle Script handle to test.
- */
- private function assert_is_script_enqueued( string $handle ): void {
- $this->assert_script_statuses( $handle, array( 'enqueued' ) );
- }
-
- /**
- * Asserts multiple enqueuing statuses for a script.
- *
- * @param string $handle Script handle to test.
- * @param array $assert_true Optional. Statuses that should assert to true. Accepts 'enqueued',
- * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array.
- * @param array $assert_false Optional. Statuses that should assert to false. Accepts 'enqueued',
- * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array.
- *
- * @throws RiskyTestError If no assertions ($assert_true, $assert_false) get passed to the function.
- */
- public function assert_script_statuses( string $handle, array $assert_true = array(), array $assert_false = array() ): void {
- if ( 0 === count( $assert_true ) + count( $assert_false ) ) {
- throw new RiskyTestError( 'Function assert_script_statuses() has been used without any arguments' );
- }
-
- foreach ( $assert_true as $status ) {
- self::assertTrue(
- wp_script_is( $handle, $status ),
- "Unexpected script status: $handle status should be '$status'"
- );
- }
-
- foreach ( $assert_false as $status ) {
- self::assertFalse(
- wp_script_is( $handle, $status ),
- "Unexpected script status: $handle status should NOT be '$status'"
- );
- }
- }
}
diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php
index 02722686b..a56396761 100644
--- a/tests/Integration/TestCase.php
+++ b/tests/Integration/TestCase.php
@@ -9,10 +9,20 @@
namespace Parsely\Tests\Integration;
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use ReflectionClass;
+use ReflectionProperty;
+use ReflectionMethod;
use Parsely\Parsely;
-use WP_Error;
+use PHPUnit\Framework\RiskyTestError;
+use WP_Post;
+use WP_Term;
use Yoast\WPTestUtils\WPIntegration\TestCase as WPIntegrationTestCase;
+use const Parsely\Utils\WP_DATE_TIME_FORMAT;
+
/**
* Abstract base class for all test case implementations.
*/
@@ -52,13 +62,13 @@ abstract class TestCase extends WPIntegrationTestCase {
'metadata_secret' => '',
'parsely_wipe_metadata_cache' => false,
'disable_autotrack' => false,
+ 'plugin_version' => '',
);
/**
* Updates Parse.ly options with a merge of default and custom values.
*
- * @param array $custom_options Associative array of option keys and values
- * to be saved.
+ * @param array $custom_options Associative array of option keys and values to be saved.
*/
public static function set_options( array $custom_options = array() ): void {
update_option( Parsely::OPTIONS_KEY, array_merge( self::DEFAULT_OPTIONS, $custom_options ) );
@@ -70,7 +80,7 @@ public static function set_options( array $custom_options = array() ): void {
* @param string $post_type Optional. The post's type. Default is 'post'.
* @param string $post_status Optional. The post's status. Default is 'publish'.
*
- * @return array An array of WP_Post fields.
+ * @return array An array of WP_Post fields.
*/
public function create_test_post_array( string $post_type = 'post', string $post_status = 'publish' ): array {
return array(
@@ -86,8 +96,8 @@ public function create_test_post_array( string $post_type = 'post', string $post
* Creates a test category.
*
* @param string $name Category name.
- * @return array|WP_Error Array containing the term_id and term_taxonomy_id,
- * WP_Error otherwise.
+ *
+ * @return int
*/
public function create_test_category( string $name ) {
return self::factory()->category->create(
@@ -104,11 +114,17 @@ public function create_test_category( string $name ) {
* Creates a test user.
*
* @param string $user_login The user's login username.
- * @return int|WP_Error The newly created user's ID or a WP_Error object
- * if the user could not be created.
+ * @param string $user_role The user's role. Default is subscriber.
+ *
+ * @return int The newly created user's ID.
*/
- public function create_test_user( string $user_login ) {
- return self::factory()->user->create( array( 'user_login' => $user_login ) );
+ public function create_test_user( string $user_login, string $user_role = 'subscriber' ) {
+ return self::factory()->user->create(
+ array(
+ 'user_login' => $user_login,
+ 'role' => $user_role,
+ )
+ );
}
/**
@@ -117,7 +133,8 @@ public function create_test_user( string $user_login ) {
* @param string $domain Site second-level domain without a .com TLD e.g. 'example' will
* result in a new subsite of 'http://example.com'.
* @param int $user_id User ID for the site administrator.
- * @return int|WP_Error The site ID on success, WP_Error object on failure.
+ *
+ * @return int
*/
public function create_test_blog( string $domain, int $user_id ) {
return self::factory()->blog->create(
@@ -133,8 +150,8 @@ public function create_test_blog( string $domain, int $user_id ) {
*
* @param string $taxonomy_key Taxonomy key, must not exceed 32 characters.
* @param string $term_name The term name to add.
- * @return array|WP_Error An array containing the term_id and term_taxonomy_id,
- * WP_Error otherwise.
+ *
+ * @return int
*/
public function create_test_taxonomy( string $taxonomy_key, string $term_name ) {
register_taxonomy(
@@ -163,9 +180,251 @@ public function create_test_taxonomy( string $taxonomy_key, string $term_name )
*/
public function create_test_post( string $post_status = 'publish' ): int {
$post_data = $this->create_test_post_array( 'post', $post_status );
- $post_id = self::factory()->post->create( $post_data );
- return $post_id;
+ return self::factory()->post->create( $post_data );
+ }
+
+ /**
+ * Creates test posts in sequence.
+ *
+ * @param int $num_of_posts Optional. Number of posts we need to create.
+ * @param string $post_type Optional. Type of the posts.
+ * @param string $post_status Optional. Status of the posts.
+ *
+ * @return WP_Post[]
+ */
+ public function create_and_get_test_posts( int $num_of_posts = 1, $post_type = 'post', $post_status = 'publish' ) {
+ $post_ids = $this->create_posts_and_get_ids( $num_of_posts, $post_type, $post_status );
+
+ return $this->get_test_posts( $post_ids );
+ }
+
+ /**
+ * Creates test posts in sequence.
+ *
+ * @param int $num_of_posts Optional. Number of posts we need to create.
+ * @param string $post_type Optional. Type of the posts.
+ * @param string $post_status Optional. Status of the posts.
+ *
+ * @return int[]
+ */
+ private function create_posts_and_get_ids( int $num_of_posts = 1, $post_type = 'post', $post_status = 'publish' ) {
+ /**
+ * Variable.
+ *
+ * @var int[]
+ */
+ $post_ids = array();
+
+ /**
+ * Variable.
+ *
+ * @var DateTime
+ */
+ $date = new DateTime( '2009-12-31', new DateTimeZone( 'America/New_York' ) ); // Date with timezone to replicate real world scenarios.
+
+ /**
+ * Variable.
+ *
+ * @var DateInterval
+ */
+ $one_day_interval = date_interval_create_from_date_string( '1 days' );
+
+ for ( $i = 1; $i <= $num_of_posts; $i++ ) {
+ /**
+ * Variable.
+ *
+ * @var DateTime
+ */
+ $post_date = date_add( $date, $one_day_interval ); // Like sequence increment by 1 day.
+ $post_id = self::factory()->post->create(
+ array(
+ 'post_type' => $post_type,
+ 'post_status' => $post_status,
+ 'post_title' => "Title $i-($post_status)",
+ 'post_author' => $i,
+ 'post_content' => "Content $i",
+ 'post_date' => $post_date->format( WP_DATE_TIME_FORMAT ),
+ 'post_date_gmt' => gmdate( WP_DATE_TIME_FORMAT, $post_date->getTimestamp() ),
+ )
+ );
+
+ array_push( $post_ids, $post_id );
+ }
+
+ return $post_ids;
+ }
+
+ /**
+ * Wrapper around get_post function which must return WP_Post.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param int $post_id Optional. Defaults to global $post.
+ *
+ * @return WP_Post
+ */
+ public function get_post( $post_id = null ) {
+ if ( null === $post_id ) {
+ global $post;
+ $post_obj = $post;
+ } else {
+ $post_obj = get_post( $post_id );
+ }
+
+ /**
+ * Variable.
+ *
+ * @var WP_Post
+ */
+ return $post_obj;
+ }
+
+ /**
+ * Wrapper around get_post function which must return WP_Post as an associative array.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param int $post_id ID of the posts.
+ *
+ * @return array
+ */
+ public function get_post_in_array( $post_id ) {
+ /**
+ * Variable.
+ *
+ * @var array
+ */
+ return get_post( $post_id, 'ARRAY_A' );
+ }
+
+ /**
+ * Gets given test posts.
+ *
+ * @param int[] $post_ids IDs of the posts.
+ *
+ * @return WP_Post[]
+ */
+ private function get_test_posts( $post_ids = array() ) {
+ $posts = array();
+
+ foreach ( $post_ids as $post_id ) {
+ array_push( $posts, get_post( $post_id ) );
+ }
+
+ /**
+ * Variable.
+ *
+ * @var WP_Post[]
+ */
+ return $posts;
+ }
+
+ /**
+ * Wrapper around get_permalink function which must return url.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param int $post_id ID of the post.
+ *
+ * @return string
+ */
+ public function get_permalink( $post_id ) {
+ /**
+ * Variable.
+ *
+ * @var string
+ */
+ return get_permalink( $post_id );
+ }
+
+ /**
+ * Wrapper around get_term function which must return WP_Term.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param int $term_id ID of the term.
+ *
+ * @return WP_Term
+ */
+ public function get_term( $term_id ) {
+ /**
+ * Variable.
+ *
+ * @var WP_Term
+ */
+ return get_term( $term_id );
+ }
+
+ /**
+ * Wrapper around get_term function which must return WP_Term in associative array.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param int $term_id ID of the term.
+ *
+ * @return array
+ */
+ public function get_term_in_array( $term_id ) {
+ /**
+ * Variable.
+ *
+ * @var array
+ */
+ return get_term( $term_id, '', 'ARRAY_A' );
+ }
+
+ /**
+ * Wrapper around get_term_link function which must return url.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param int $term_id ID of the term.
+ *
+ * @return string
+ */
+ public function get_term_link( $term_id ) {
+ /**
+ * Variable.
+ *
+ * @var string
+ */
+ return get_term_link( $term_id );
+ }
+
+ /**
+ * Wrapper around get_post_time function which must return time in int.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param string $format Format to use for retrieving the time.
+ * @param bool $is_gmt Whether to retrieve the GMT time.
+ * @param int|WP_Post $post WP_Post object or ID.
+ *
+ * @return int
+ */
+ public function get_post_time_in_int( $format, $is_gmt, $post ) {
+ /**
+ * Variable.
+ *
+ * @var int
+ */
+ return get_post_time( $format, $is_gmt, $post );
+ }
+
+ /**
+ * Wrapper around wp_json_encode function which must return string.
+ *
+ * This function ensures strict typing in our codebase.
+ *
+ * @param mixed $data — Variable (usually an array or object) to encode as JSON.
+ *
+ * @return string
+ */
+ public function wp_json_encode( $data ) {
+ $encoded_data = wp_json_encode( $data );
+
+ return false !== $encoded_data ? $encoded_data : '';
}
/**
@@ -187,10 +446,240 @@ public function go_to_new_post( string $post_status = 'publish' ): int {
*
* @param int $admin_user_id User ID for the site administrator.
* Default is 1 which is assigned to first admin user while creating the site.
- *
- * @return void
*/
public function set_admin_user( $admin_user_id = 1 ): void {
wp_set_current_user( $admin_user_id );
}
+
+ /**
+ * Creates a user with role `contributor` and login.
+ */
+ public function login_as_contributor(): void {
+ $user_id = $this->create_test_user( 'test_contributor', 'contributor' );
+ wp_set_current_user( $user_id );
+ }
+
+ /**
+ * Verifies that given hooks are called or not.
+ *
+ * @param string[] $hooks WordPress hooks whose availability we have to verify.
+ * @param bool $availability_type TRUE if we want to check the presence of given hooks.
+ */
+ public function assert_wp_hooks_availablility( $hooks, $availability_type ): void {
+ if ( ! $this->is_php_version_7dot2_or_higher() ) {
+ return;
+ }
+
+ if ( true === $availability_type ) {
+ $this->assert_wp_hooks( $hooks );
+ } else {
+ $this->assert_wp_hooks( array(), $hooks );
+ }
+ }
+
+ /**
+ * Asserts WordPress hooks.
+ *
+ * @param string[] $true_hooks Optional. Actions that should have been present.
+ * @param string[] $false_hooks Optional. Actions that should have not been present.
+ *
+ * @throws RiskyTestError If no assertions get passed to the function.
+ */
+ private function assert_wp_hooks( array $true_hooks = array(), array $false_hooks = array() ): void {
+ if ( 0 === count( $true_hooks ) + count( $false_hooks ) ) {
+ throw new RiskyTestError( 'Function assert_wp_hooks() has been used without any arguments' );
+ }
+
+ foreach ( $true_hooks as $hook ) {
+ self::assertTrue(
+ has_action( $hook ),
+ "Unexpected hook status: $hook should have been called."
+ );
+ }
+
+ foreach ( $false_hooks as $hook ) {
+ self::assertFalse(
+ has_action( $hook ),
+ "Unexpected hook status: $hook should have not been called."
+ );
+ }
+ }
+
+ /**
+ * Asserts that a passed script is not registered.
+ *
+ * @param string $handle Script handle to test.
+ */
+ public function assert_is_script_not_registered( string $handle ): void {
+ $this->assert_script_statuses( $handle, array(), array( 'registered' ) );
+ }
+
+ /**
+ * Asserts that a passed script is registered.
+ *
+ * @param string $handle Script handle to test.
+ */
+ public function assert_is_script_registered( string $handle ): void {
+ $this->assert_script_statuses( $handle, array( 'registered' ) );
+ }
+
+ /**
+ * Asserts that a passed script is not enqueued.
+ *
+ * @param string $handle Script handle to test.
+ */
+ public function assert_is_script_not_enqueued( string $handle ): void {
+ $this->assert_script_statuses( $handle, array(), array( 'enqueued' ) );
+ }
+
+ /**
+ * Asserts that a passed script is enqueued.
+ *
+ * @param string $handle Script handle to test.
+ */
+ public function assert_is_script_enqueued( string $handle ): void {
+ $this->assert_script_statuses( $handle, array( 'enqueued' ) );
+ }
+
+ /**
+ * Asserts multiple enqueuing statuses for a script.
+ *
+ * @param string $handle Script handle to test.
+ * @param array $assert_true Optional. Statuses that should assert to true. Accepts 'enqueued',
+ * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array.
+ * @param array $assert_false Optional. Statuses that should assert to false. Accepts 'enqueued',
+ * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array.
+ *
+ * @throws RiskyTestError If no assertions ($assert_true, $assert_false) get passed to the function.
+ */
+ private function assert_script_statuses( string $handle, array $assert_true = array(), array $assert_false = array() ): void {
+ if ( 0 === count( $assert_true ) + count( $assert_false ) ) {
+ throw new RiskyTestError( 'Function assert_script_statuses() has been used without any arguments' );
+ }
+
+ foreach ( $assert_true as $status ) {
+ self::assertTrue(
+ wp_script_is( $handle, $status ),
+ "Unexpected script status: $handle status should be '$status'"
+ );
+ }
+
+ foreach ( $assert_false as $status ) {
+ self::assertFalse(
+ wp_script_is( $handle, $status ),
+ "Unexpected script status: $handle status should NOT be '$status'"
+ );
+ }
+ }
+
+ /**
+ * Asserts that a passed style is not registered.
+ *
+ * @param string $handle Style handle to test.
+ */
+ public function assert_is_style_not_registered( string $handle ): void {
+ $this->assert_style_statuses( $handle, array(), array( 'registered' ) );
+ }
+
+ /**
+ * Asserts that a passed style is registered.
+ *
+ * @param string $handle Style handle to test.
+ */
+ public function assert_is_style_registered( string $handle ): void {
+ $this->assert_style_statuses( $handle, array( 'registered' ) );
+ }
+
+ /**
+ * Asserts that a passed style is not enqueued.
+ *
+ * @param string $handle Style handle to test.
+ */
+ public function assert_is_style_not_enqueued( string $handle ): void {
+ $this->assert_style_statuses( $handle, array(), array( 'enqueued' ) );
+ }
+
+ /**
+ * Asserts that a passed style is enqueued.
+ *
+ * @param string $handle Style handle to test.
+ */
+ public function assert_is_style_enqueued( string $handle ): void {
+ $this->assert_style_statuses( $handle, array( 'enqueued' ) );
+ }
+
+ /**
+ * Asserts multiple enqueuing statuses for a style.
+ *
+ * @param string $handle Style handle to test.
+ * @param array $assert_true Optional. Statuses that should assert to true. Accepts 'enqueued',
+ * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array.
+ * @param array $assert_false Optional. Statuses that should assert to false. Accepts 'enqueued',
+ * 'registered', 'queue', 'to_do', and 'done'. Default is an empty array.
+ *
+ * @throws RiskyTestError If no assertions ($assert_true, $assert_false) get passed to the function.
+ */
+ private function assert_style_statuses( string $handle, array $assert_true = array(), array $assert_false = array() ): void {
+ if ( 0 === count( $assert_true ) + count( $assert_false ) ) {
+ throw new RiskyTestError( 'Function assert_style_statuses() has been used without any arguments' );
+ }
+
+ foreach ( $assert_true as $status ) {
+ self::assertTrue(
+ wp_style_is( $handle, $status ),
+ "Unexpected style status: $handle status should be '$status'"
+ );
+ }
+
+ foreach ( $assert_false as $status ) {
+ self::assertFalse(
+ wp_style_is( $handle, $status ),
+ "Unexpected style status: $handle status should NOT be '$status'"
+ );
+ }
+ }
+
+ /**
+ * Returns TRUE if minimum PHP version is 7.2 or higher. We uses this if something works
+ * differently in PHP versions < 7.2 and >= 7.2.
+ *
+ * Note: Remove this function when we remove support for PHP 7.1.
+ */
+ public function is_php_version_7dot2_or_higher(): bool {
+ return phpversion() >= '7.2';
+ }
+
+ /**
+ * Gets private property of a class.
+ *
+ * @param class-string $class_name Name of the class.
+ * @param string $property_name Name of the property.
+ *
+ * @return ReflectionProperty
+ */
+ public function get_private_property( $class_name, $property_name ) {
+ $reflector = new ReflectionClass( $class_name );
+ $property = $reflector->getProperty( $property_name );
+
+ $property->setAccessible( true );
+
+ return $property;
+ }
+
+ /**
+ * Gets private method of a class.
+ *
+ * @param class-string $class_name Name of the class.
+ * @param string $method Name of the method.
+ *
+ * @return ReflectionMethod
+ */
+ public function get_private_method( $class_name, $method ) {
+ $reflector = new ReflectionClass( $class_name );
+ $method = $reflector->getMethod( $method );
+
+ $method->setAccessible( true );
+
+ return $method;
+ }
}
diff --git a/tests/Integration/UI/AdminColumnsParselyStatsTest.php b/tests/Integration/UI/AdminColumnsParselyStatsTest.php
new file mode 100644
index 000000000..353ce1ee4
--- /dev/null
+++ b/tests/Integration/UI/AdminColumnsParselyStatsTest.php
@@ -0,0 +1,1240 @@
+ array(),
+ 'error' => null,
+ );
+
+ /**
+ * Setup method called before each test.
+ */
+ public function set_up(): void {
+ parent::set_up();
+
+ $this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%' );
+ $this->set_admin_user();
+ }
+
+ /**
+ * Teardown method called after each test.
+ */
+ public function tear_down(): void {
+ parent::tear_down();
+
+ $this->set_permalink_structure( '' );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats styles.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles
+ */
+ public function test_styles_of_parsely_stats_admin_column_on_empty_plugin_options(): void {
+ $this->set_empty_plugin_options();
+ $this->assert_parsely_stats_admin_styles( false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats styles.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles
+ */
+ public function test_styles_of_parsely_stats_admin_column_on_empty_api_secret(): void {
+ $this->set_empty_api_secret();
+ $this->assert_parsely_stats_admin_styles( true );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats styles.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles
+ */
+ public function test_styles_of_parsely_stats_admin_column_on_empty_track_post_types(): void {
+ $this->set_empty_track_post_types();
+ $this->assert_parsely_stats_admin_styles( false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats styles.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles
+ */
+ public function test_styles_of_parsely_stats_admin_column_on_invalid_track_post_type(): void {
+ $this->set_valid_plugin_options();
+ set_current_screen( 'edit-page' );
+ $this->assert_parsely_stats_admin_styles( false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats styles.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_styles
+ */
+ public function test_styles_of_parsely_stats_admin_column_on_valid_posts(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+ $this->assert_parsely_stats_admin_styles( true );
+ }
+
+ /**
+ * Asserts on Parse.ly Stats styles.
+ *
+ * @param bool $assert_type Indicates wether we are asserting for TRUE or FALSE.
+ */
+ private function assert_parsely_stats_admin_styles( bool $assert_type ): void {
+ $obj = $this->init_admin_columns_parsely_stats();
+
+ if ( $this->is_php_version_7dot2_or_higher() ) {
+ do_action( 'current_screen' ); // phpcs:ignore
+ do_action( 'admin_enqueue_scripts' ); // phpcs:ignore
+ } else {
+ $obj->set_current_screen();
+ $obj->enqueue_parsely_stats_styles();
+ }
+
+ $handle = 'admin-parsely-stats-styles';
+ if ( $assert_type ) {
+ $this->assert_is_style_enqueued( $handle );
+ wp_dequeue_style( $handle ); // Dequeue to start fresh for next test.
+ } else {
+ $this->assert_is_style_not_enqueued( $handle );
+ }
+ }
+
+ /**
+ * Verifies Parse.ly Stats column visibility.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view
+ */
+ public function test_parsely_stats_column_visibility_on_empty_plugin_options(): void {
+ $this->set_empty_plugin_options();
+
+ $this->assert_hooks_for_parsely_stats_column( false );
+ self::assertNotContains( self::$parsely_stats_column_header, $this->get_admin_columns() );
+ }
+
+
+ /**
+ * Verifies Parse.ly Stats column visibility.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view
+ */
+ public function test_parsely_stats_column_visibility_on_empty_api_secret(): void {
+ $this->set_empty_api_secret();
+
+ self::assertContains( self::$parsely_stats_column_header, $this->get_admin_columns() );
+ $this->assert_hooks_for_parsely_stats_column( true );
+ }
+
+ /**
+ * Verifies Parse.ly Stats column visibility.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view
+ */
+ public function test_parsely_stats_column_visibility_on_empty_track_post_types(): void {
+ $this->set_empty_track_post_types();
+
+ self::assertNotContains( self::$parsely_stats_column_header, $this->get_admin_columns() );
+ $this->assert_hooks_for_parsely_stats_column( true );
+ }
+
+ /**
+ * Verifies Parse.ly Stats column visibility.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view
+ */
+ public function test_parsely_stats_column_visibility_on_invalid_track_post_types(): void {
+ $this->set_valid_plugin_options();
+ set_current_screen( 'edit-page' );
+
+ self::assertNotContains( self::$parsely_stats_column_header, $this->get_admin_columns() );
+ $this->assert_hooks_for_parsely_stats_column( true );
+ }
+
+ /**
+ * Verifies Parse.ly Stats column visibility.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view
+ */
+ public function test_parsely_stats_column_visibility_on_valid_posts(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ self::assertContains( self::$parsely_stats_column_header, $this->get_admin_columns() );
+ $this->assert_hooks_for_parsely_stats_column( true );
+ }
+
+ /**
+ * Verifies Parse.ly Stats column visibility.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::add_parsely_stats_column_on_list_view
+ */
+ public function test_parsely_stats_column_visibility_on_valid_pages(): void {
+ $this->set_valid_conditions_for_parsely_stats( 'page' );
+
+ self::assertContains( self::$parsely_stats_column_header, $this->get_admin_columns() );
+ $this->assert_hooks_for_parsely_stats_column( true );
+ }
+
+ /**
+ * Gets Admin Columns.
+ *
+ * @return array
+ */
+ private function get_admin_columns() {
+ $obj = $this->init_admin_columns_parsely_stats();
+
+ if ( $this->is_php_version_7dot2_or_higher() ) {
+ do_action( 'current_screen' ); // phpcs:ignore
+ } else {
+ $obj->set_current_screen();
+ }
+
+ return $obj->add_parsely_stats_column_on_list_view( array() );
+ }
+
+ /**
+ * Asserts status of hooks for Parse.ly Stats column.
+ *
+ * @param bool $assert_type Assert this condition on hooks.
+ */
+ private function assert_hooks_for_parsely_stats_column( $assert_type ): void {
+ $this->assert_wp_hooks_availablility(
+ array( 'current_screen', 'manage_posts_columns', 'manage_pages_columns' ),
+ $assert_type
+ );
+ }
+
+ /**
+ * Verifies content of Parse.ly Stats column.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder
+ */
+ public function test_content_of_parsely_stats_column_on_empty_plugin_options(): void {
+ $this->set_empty_plugin_options();
+
+ $obj = $this->init_admin_columns_parsely_stats();
+ $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj );
+
+ $this->assert_hooks_for_parsely_stats_content( false );
+ self::assertEquals( '', $output );
+ self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) );
+ }
+
+ /**
+ * Verifies content of Parse.ly Stats column.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder
+ */
+ public function test_content_of_parsely_stats_column_on_empty_api_secret(): void {
+ $this->set_empty_api_secret();
+
+ $obj = $this->init_admin_columns_parsely_stats();
+ $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj );
+
+ $this->assert_hooks_for_parsely_stats_content( true );
+ self::assertEquals(
+ $this->get_parsely_stats_placeholder_content( '/2010/01/01/title-1-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/2010/01/02/title-2-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/2010/01/03/title-3-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/' ) .
+ $this->get_parsely_stats_placeholder_content( '/' ),
+ $output
+ );
+ self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) );
+ }
+
+ /**
+ * Verifies content of Parse.ly Stats column.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder
+ */
+ public function test_content_of_parsely_stats_column_on_empty_track_post_types(): void {
+ $this->set_empty_track_post_types();
+
+ $obj = $this->init_admin_columns_parsely_stats();
+ $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj );
+
+ $this->assert_hooks_for_parsely_stats_content( true );
+ self::assertEquals( '', $output );
+ self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) );
+ }
+
+ /**
+ * Verifies content of Parse.ly Stats column.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder
+ */
+ public function test_content_of_parsely_stats_column_on_invalid_track_post_types(): void {
+ $this->set_valid_plugin_options();
+ set_current_screen( 'edit-page' );
+
+ $obj = $this->init_admin_columns_parsely_stats();
+ $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj );
+
+ $this->assert_hooks_for_parsely_stats_content( true );
+ self::assertEquals( '', $output );
+ self::assertEquals( array(), $this->get_utc_published_times_property( $obj ) );
+ }
+
+ /**
+ * Verifies content of Parse.ly Stats column.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder
+ */
+ public function test_content_of_parsely_stats_column_on_valid_posts(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $obj = $this->init_admin_columns_parsely_stats();
+ $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj );
+
+ $this->assert_hooks_for_parsely_stats_content( true );
+ self::assertEquals(
+ $this->get_parsely_stats_placeholder_content( '/2010/01/01/title-1-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/2010/01/02/title-2-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/2010/01/03/title-3-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/' ) .
+ $this->get_parsely_stats_placeholder_content( '/' ),
+ $output
+ );
+ self::assertEquals(
+ array( '2010-01-01 05:00:00', '2010-01-02 05:00:00', '2010-01-03 05:00:00' ),
+ $this->get_utc_published_times_property( $obj )
+ );
+ }
+
+
+ /**
+ * Verifies content of Parse.ly Stats column.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::update_published_times_and_show_placeholder
+ */
+ public function test_content_of_parsely_stats_column_on_valid_pages(): void {
+ $this->set_valid_conditions_for_parsely_stats( 'page' );
+
+ $obj = $this->init_admin_columns_parsely_stats();
+ $output = $this->set_posts_data_and_get_content_of_parsely_stats_column( $obj );
+
+ $this->assert_hooks_for_parsely_stats_content( true );
+ self::assertEquals(
+ $this->get_parsely_stats_placeholder_content( '/2010/01/01/title-1-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/2010/01/02/title-2-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/2010/01/03/title-3-publish' ) .
+ $this->get_parsely_stats_placeholder_content( '/' ) .
+ $this->get_parsely_stats_placeholder_content( '/' ),
+ $output
+ );
+ self::assertEquals(
+ array( '2010-01-01 05:00:00', '2010-01-02 05:00:00', '2010-01-03 05:00:00' ),
+ $this->get_utc_published_times_property( $obj )
+ );
+ }
+
+ /**
+ * Sets posts data and get content of Parse.ly Stats column.
+ *
+ * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats.
+ * @param string $post_type Type of the post.
+ *
+ * @return string
+ */
+ private function set_posts_data_and_get_content_of_parsely_stats_column( $obj, $post_type = 'post' ) {
+ $posts = $this->set_and_get_posts_data( 3, 2, $post_type );
+
+ return $this->get_content_of_parsely_stats_column( $obj, $posts, $post_type );
+ }
+
+ /**
+ * Sets posts data.
+ *
+ * @param int $publish_num_of_posts Number of publish posts that we have to create.
+ * @param int $draft_num_of_posts Number of draft posts that we have to create.
+ * @param string $post_type Type of the post.
+ *
+ * @return WP_Post[]
+ */
+ private function set_and_get_posts_data( $publish_num_of_posts = 1, $draft_num_of_posts = 0, $post_type = 'post' ) {
+ return array_merge(
+ $this->create_and_get_test_posts( $publish_num_of_posts ),
+ $this->create_and_get_test_posts( $draft_num_of_posts, $post_type, 'draft' )
+ );
+ }
+
+ /**
+ * Gets content of Parse.ly Stats column.
+ *
+ * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats.
+ * @param WP_Post[] $posts Available posts.
+ * @param string $post_type Type of the post.
+ *
+ * @return string
+ */
+ private function get_content_of_parsely_stats_column( $obj, $posts, $post_type ) {
+ ob_start();
+ $this->show_content_on_parsely_stats_column( $obj, $posts, $post_type );
+
+ return (string) ob_get_clean();
+ }
+
+ /**
+ * Replicates behavior by which WordPress set post publish dates and then make API call
+ * to get Parse.ly stats.
+ *
+ * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats.
+ * @param WP_Post[] $posts Available posts.
+ * @param string $post_type Type of the post.
+ */
+ private function show_content_on_parsely_stats_column( $obj, $posts, $post_type ): void {
+ if ( $this->is_php_version_7dot2_or_higher() ) {
+ do_action( 'current_screen' ); // phpcs:ignore
+ } else {
+ $obj->set_current_screen();
+ }
+
+ foreach ( $posts as $current_post ) {
+ global $post;
+ $post = $current_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ $column_name = 'parsely-stats';
+
+ if ( $this->is_php_version_7dot2_or_higher() ) {
+ do_action( "manage_{$post_type}s_custom_column", $column_name ); // phpcs:ignore
+ } else {
+ $obj->update_published_times_and_show_placeholder( $column_name );
+ }
+ }
+ }
+
+ /**
+ * Gets placeholder content of Parse.ly stats column.
+ *
+ * @param string $key Stats Key.
+ *
+ * @return string
+ */
+ private function get_parsely_stats_placeholder_content( $key ) {
+ return " \n ... \n
\n ";
+ }
+
+ /**
+ * Gets utc_published_times property of given object.
+ *
+ * @param Admin_Columns_Parsely_Stats $obj Instance of Admin_Columns_Parsely_Stats.
+ *
+ * @return string
+ */
+ private function get_utc_published_times_property( $obj ) {
+ /**
+ * Variable.
+ *
+ * @var string
+ */
+ return $this->get_private_property( Admin_Columns_Parsely_Stats::class, 'utc_published_times' )->getValue( $obj );
+ }
+
+ /**
+ * Asserts status of hooks for showing Parse.ly Stats content inside column.
+ *
+ * @param bool $assert_type Assert this condition on hooks.
+ */
+ private function assert_hooks_for_parsely_stats_content( $assert_type = true ): void {
+ $this->assert_wp_hooks_availablility(
+ array( 'current_screen', 'manage_posts_custom_column', 'manage_pages_custom_column' ),
+ $assert_type
+ );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data
+ */
+ public function test_script_of_parsely_stats_admin_column_on_empty_plugin_options(): void {
+ $this->set_empty_plugin_options();
+ $obj = $this->mock_parsely_stats_response( null );
+ $this->assert_parsely_stats_admin_script( $obj, false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data
+ */
+ public function test_script_of_parsely_stats_admin_column_on_empty_api_secret(): void {
+ $this->set_empty_api_secret();
+ $obj = $this->mock_parsely_stats_response( array() );
+ $this->assert_parsely_stats_admin_script( $obj, true );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data
+ */
+ public function test_script_of_parsely_stats_admin_column_on_empty_track_post_types(): void {
+ $this->set_empty_track_post_types();
+ $obj = $this->mock_parsely_stats_response( null );
+ $this->assert_parsely_stats_admin_script( $obj, false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data
+ */
+ public function test_script_of_parsely_stats_admin_column_on_invalid_track_post_types(): void {
+ $this->set_valid_plugin_options();
+ set_current_screen( 'edit-page' );
+
+ $obj = $this->mock_parsely_stats_response( null );
+ $this->assert_parsely_stats_admin_script( $obj, false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data
+ */
+ public function test_script_of_parsely_stats_admin_column_on_valid_posts_and_empty_response(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $obj = $this->mock_parsely_stats_response( null );
+ $this->assert_parsely_stats_admin_script( $obj, false );
+ }
+
+ /**
+ * Verifies enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::enqueue_parsely_stats_script_with_data
+ */
+ public function test_script_of_parsely_stats_admin_column_on_valid_posts_and_valid_response(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $obj = $this->mock_parsely_stats_response( array() );
+ $this->assert_parsely_stats_admin_script( $obj, true );
+
+ /**
+ * Internal Variable.
+ *
+ * @var WP_Scripts
+ */
+ global $wp_scripts;
+
+ ob_start();
+ var_dump( $wp_scripts->print_inline_script ( 'admin-parsely-stats-script', 'before' ) ); // phpcs:ignore
+ $output = (string) ob_get_clean();
+
+ self::assertStringContainsString( 'window.wpParselyPostsStatsResponse = \'[]\';', $output );
+ }
+
+ /**
+ * Mock function get_parsely_stats_response from class.
+ *
+ * @param null|array $return_value Value that we have to return from mock function.
+ *
+ * @return Admin_Columns_Parsely_Stats
+ */
+ private function mock_parsely_stats_response( $return_value ) {
+ $obj = Mockery::mock( Admin_Columns_Parsely_Stats::class, array( new Parsely() ) )->makePartial();
+ $obj->shouldReceive( 'get_parsely_stats_response' )->once()->andReturn( $return_value );
+ $obj->run();
+
+ return $obj;
+ }
+
+
+ /**
+ * Verifies Parse.ly API call and enqueued status of Parse.ly Stats script.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ */
+ public function test_should_not_call_parsely_api_on_empty_api_secret_and_hidden_parsely_stats_column(): void {
+ $this->set_empty_api_secret();
+
+ $obj = $this->mock_is_parsely_stats_column_hidden( true );
+ $this->assert_parsely_stats_admin_script( $obj, false );
+ }
+
+ /**
+ * Mock function is_parsely_stats_column_hidden from class.
+ *
+ * @param bool $return_value Value that we have to return from mock function.
+ *
+ * @return Admin_Columns_Parsely_Stats
+ */
+ private function mock_is_parsely_stats_column_hidden( $return_value = false ) {
+ $obj = Mockery::mock( Admin_Columns_Parsely_Stats::class, array( new Parsely() ) )->makePartial();
+ $obj->shouldReceive( 'is_parsely_stats_column_hidden' )->once()->andReturn( $return_value );
+ $obj->run();
+
+ return $obj;
+ }
+
+ /**
+ * Asserts script of Parse.ly Stats.
+ *
+ * @param Admin_Columns_Parsely_Stats $obj Instance of the class.
+ * @param bool $assert_type Indicates wether we are asserting for TRUE or FALSE.
+ */
+ private function assert_parsely_stats_admin_script( $obj, $assert_type ): void {
+ if ( $this->is_php_version_7dot2_or_higher() ) {
+ do_action( 'current_screen' ); // phpcs:ignore
+ do_action( 'admin_footer' ); // phpcs:ignore
+ } else {
+ $obj->set_current_screen();
+ $obj->enqueue_parsely_stats_script_with_data();
+ }
+
+ $handle = 'admin-parsely-stats-script';
+ if ( $assert_type ) {
+ $this->assert_is_script_enqueued( $handle );
+ wp_dequeue_script( $handle ); // Dequeue to start fresh for next test.
+ } else {
+ $this->assert_is_script_not_enqueued( $handle );
+ }
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ */
+ public function test_parsely_stats_response_on_empty_plugin_options(): void {
+ $this->set_empty_plugin_options();
+
+ $res = $this->get_parsely_stats_response();
+
+ $this->assert_hooks_for_parsely_stats_response( false );
+ self::assertNull( $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ */
+ public function test_parsely_stats_response_on_empty_api_secret(): void {
+ $this->set_empty_api_secret();
+
+ $res = $this->get_parsely_stats_response();
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertEquals(
+ array(
+ 'data' => null,
+ 'error' => array(
+ 'code' => 403,
+ 'message' => 'Forbidden.',
+ 'htmlMessage' => '' .
+ 'We are unable to retrieve data for Parse.ly Stats. ' .
+ 'Please contact support@parsely.com for help resolving this issue.' .
+ '
',
+ ),
+ ),
+ $res
+ );
+ }
+
+ /**
+ * Verifies Parse.ly Stats API arguments.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api
+ */
+ public function test_api_params_of_analytics_api_call_on_valid_post_type_and_having_single_record(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $posts = $this->set_and_get_posts_data( 1, 2 );
+ $res = $this->get_parsely_stats_response(
+ $posts,
+ 'post',
+ null,
+ array(
+ 'pub_date_start' => '2010-01-01',
+ 'pub_date_end' => '2010-01-01',
+ )
+ );
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertEquals( self::$parsely_api_empty_response, $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats API arguments.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api
+ */
+ public function test_api_params_of_analytics_api_call_on_valid_post_type_and_having_multiple_records(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $posts = $this->set_and_get_posts_data( 3, 5 );
+ $res = $this->get_parsely_stats_response(
+ $posts,
+ 'post',
+ null,
+ array(
+ 'pub_date_start' => '2010-01-01',
+ 'pub_date_end' => '2010-01-03',
+ )
+ );
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertEquals( self::$parsely_api_empty_response, $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ */
+ public function test_parsely_stats_response_on_empty_track_post_types(): void {
+ $this->set_empty_track_post_types();
+
+ $res = $this->get_parsely_stats_response();
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertNull( $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ */
+ public function test_parsely_stats_response_on_invalid_track_post_types(): void {
+ $this->set_valid_plugin_options();
+ set_current_screen( 'edit-page' );
+
+ $res = $this->get_parsely_stats_response();
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertNull( $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ */
+ public function test_parsely_stats_response_on_valid_post_type_and_no_post_data(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $res = $this->get_parsely_stats_response();
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertNull( $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api
+ */
+ public function test_parsely_stats_response_on_valid_post_type_and_null_response_from_api(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $posts = $this->set_and_get_posts_data();
+ $res = $this->get_parsely_stats_response( $posts );
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertEquals( self::$parsely_api_empty_response, $res );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api
+ */
+ public function test_parsely_stats_response_on_valid_post_type_and_error_response_from_api(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $posts = $this->set_and_get_posts_data( 1 );
+ $res = $this->get_parsely_stats_response( $posts, 'post', new WP_Error( 404, 'Not Found.' ) );
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertNull( isset( $res['data'] ) ? $res['data'] : null );
+ self::assertEquals(
+ array(
+ 'code' => 404,
+ 'message' => 'Not Found.',
+ 'htmlMessage' => 'Error while getting data for Parse.ly Stats. Detail: (404) Not Found.
',
+ ),
+ isset( $res['error'] ) ? $res['error'] : null
+ );
+ }
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_unique_stats_key_from_analytics
+ */
+ public function test_parsely_stats_response_on_valid_post_type_and_having_data_from_api(): void {
+ $this->set_valid_conditions_for_parsely_stats();
+
+ $posts = $this->set_and_get_posts_data( 7, 10 );
+ $api_response = array(
+ array(
+ 'url' => 'http://example.com/2010/01/01/title-1-publish',
+ 'metrics' => array(
+ 'views' => 0,
+ 'visitors' => 0,
+ 'avg_engaged' => 0,
+ ),
+ ),
+ array(
+ 'url' => 'http://example.com/2010/01/02/title-2-publish',
+ 'metrics' => array(
+ 'views' => 1,
+ 'visitors' => 1,
+ 'avg_engaged' => 0.01,
+ ),
+ ),
+ array(
+ 'url' => 'http://example.com/2010/01/03/title-3-publish',
+ 'metrics' => array(
+ 'views' => 1100,
+ 'visitors' => 1100000,
+ 'avg_engaged' => 1.1,
+ ),
+ ),
+ array(
+ 'url' => 'http://example.com/2010/01/04/title-4-publish',
+ ),
+ array(
+ 'url' => 'http://example.com/2010/01/05/title-5-publish',
+ 'metrics' => array(
+ 'views' => 1,
+ ),
+ ),
+ array(
+ 'url' => 'http://example.com/2010/01/06/title-6-publish',
+ 'metrics' => array(
+ 'visitors' => 1,
+ ),
+ ),
+ array(
+ 'url' => 'http://example.com/2010/01/07/title-7-publish',
+ 'metrics' => array(
+ 'avg_engaged' => 0.01,
+ ),
+ ),
+ );
+ $res = $this->get_parsely_stats_response(
+ $posts,
+ 'post',
+ $api_response,
+ array(
+ 'pub_date_start' => '2010-01-01',
+ 'pub_date_end' => '2010-01-07',
+ )
+ );
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertNull( isset( $res['error'] ) ? $res['error'] : null );
+ self::assertEquals(
+ array(
+ '/2010/01/01/title-1-publish' => array(
+ 'page_views' => '0 page views',
+ 'visitors' => '0 visitors',
+ 'avg_time' => '0 sec. avg time',
+ ),
+ '/2010/01/02/title-2-publish' => array(
+ 'page_views' => '1 page view',
+ 'visitors' => '1 visitor',
+ 'avg_time' => '1 sec. avg time',
+ ),
+ '/2010/01/03/title-3-publish' => array(
+ 'page_views' => '1.1K page views',
+ 'visitors' => '1.1M visitors',
+ 'avg_time' => '1:06 avg time',
+ ),
+ '/2010/01/05/title-5-publish' => array(
+ 'page_views' => '1 page view',
+ 'visitors' => '0 visitors',
+ 'avg_time' => '0 sec. avg time',
+ ),
+ '/2010/01/06/title-6-publish' => array(
+ 'page_views' => '0 page views',
+ 'visitors' => '1 visitor',
+ 'avg_time' => '0 sec. avg time',
+ ),
+ '/2010/01/07/title-7-publish' => array(
+ 'page_views' => '0 page views',
+ 'visitors' => '0 visitors',
+ 'avg_time' => '1 sec. avg time',
+ ),
+ ),
+ isset( $res['data'] ) ? $res['data'] : null
+ );
+ }
+
+
+ /**
+ * Verifies Parse.ly Stats response.
+ *
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::__construct
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::run
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::set_current_screen
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::is_tracked_as_post_type
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_parsely_stats_response
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_publish_date_params_for_analytics_api
+ * @covers \Parsely\UI\Admin_Columns_Parsely_Stats::get_unique_stats_key_from_analytics
+ */
+ public function test_parsely_stats_response_on_valid_hierarchal_post_type_and_having_data_from_api(): void {
+ $this->set_valid_conditions_for_parsely_stats( 'page' );
+
+ $pages = $this->set_and_get_posts_data( 1, 2, 'page' );
+ $api_response = array(
+ array(
+ 'url' => 'http://example.com/2010/01/01/title-1-publish',
+ 'metrics' => array(
+ 'views' => 1100,
+ 'visitors' => 1100000,
+ 'avg_engaged' => 1.1,
+ ),
+ ),
+ );
+ $res = $this->get_parsely_stats_response(
+ $pages,
+ 'page',
+ $api_response,
+ array(
+ 'pub_date_start' => '2010-01-01',
+ 'pub_date_end' => '2010-01-01',
+ )
+ );
+
+ $this->assert_hooks_for_parsely_stats_response( true );
+ self::assertNull( isset( $res['error'] ) ? $res['error'] : null );
+ self::assertEquals(
+ array(
+ '/2010/01/01/title-1-publish' => array(
+ 'page_views' => '1.1K page views',
+ 'visitors' => '1.1M visitors',
+ 'avg_time' => '1:06 avg time',
+ ),
+ ),
+ isset( $res['data'] ) ? $res['data'] : null
+ );
+ }
+
+ /**
+ * Replicates behavior by which WordPress set post publish dates and then make API call
+ * to get Parse.ly stats.
+ *
+ * @param WP_Post[] $posts Available Posts.
+ * @param string $post_type Type of the post.
+ * @param Analytics_Post[]|WP_Error|null $api_response Mocked response that we return on calling API.
+ * @param Analytics_Post_API_Params|null $api_params API Parameters.
+ *
+ * @return Parsely_Posts_Stats_Response|null
+ */
+ private function get_parsely_stats_response( $posts = array(), $post_type = 'post', $api_response = null, $api_params = null ) {
+ $obj = $this->init_admin_columns_parsely_stats();
+
+ ob_start();
+ $this->show_content_on_parsely_stats_column( $obj, $posts, $post_type );
+ ob_get_clean(); // Discard output to keep console clean while running tests.
+
+ $api = Mockery::mock( Analytics_Posts_API::class, array( new Parsely() ) )->makePartial();
+ if ( ! is_null( $api_params ) ) {
+ $api->shouldReceive( 'get_posts_analytics' )
+ ->once()
+ ->withArgs(
+ array(
+ array_merge(
+ $api_params,
+ // Params which will not change.
+ array(
+ 'period_start' => get_utc_date_format( -7 ),
+ 'period_end' => get_utc_date_format(),
+ 'limit' => 2000,
+ 'sort' => 'avg_engaged',
+ )
+ ),
+ )
+ )
+ ->andReturn( $api_response );
+ } else {
+ $api->shouldReceive( 'get_posts_analytics' )->once()->andReturn( $api_response );
+ }
+
+ return $obj->get_parsely_stats_response( $api );
+ }
+
+ /**
+ * Asserts status of hooks for Parse.ly Stats response.
+ *
+ * @param bool $assert_type Assert this condition on hooks.
+ */
+ private function assert_hooks_for_parsely_stats_response( $assert_type = true ): void {
+ $this->assert_wp_hooks_availablility(
+ array( 'current_screen', 'manage_posts_custom_column', 'manage_pages_custom_column', 'admin_footer' ),
+ $assert_type
+ );
+ }
+
+ /**
+ * Initializes Admin_Columns_Parsely_Stats object.
+ *
+ * @return Admin_Columns_Parsely_Stats
+ */
+ private function init_admin_columns_parsely_stats() {
+ $obj = new Admin_Columns_Parsely_Stats( new Parsely() );
+ $obj->run();
+
+ return $obj;
+ }
+
+ /**
+ * Sets empty key and secret.
+ */
+ private function set_empty_plugin_options(): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => '',
+ 'api_secret' => '',
+ 'track_post_types' => array(),
+ )
+ );
+
+ set_current_screen( 'edit-post' );
+ }
+
+ /**
+ * Sets empty API Secret.
+ *
+ * @param string $post_type Type of the post.
+ */
+ private function set_empty_api_secret( $post_type = 'post' ): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test',
+ 'api_secret' => '',
+ 'track_post_types' => array( $post_type ),
+ )
+ );
+
+ set_current_screen( 'edit-post' );
+ }
+
+ /**
+ * Sets empty track_post_types.
+ */
+ private function set_empty_track_post_types(): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test',
+ 'api_secret' => 'test',
+ 'track_post_types' => array(),
+ )
+ );
+
+ set_current_screen( 'edit-post' );
+ }
+
+ /**
+ * Sets valid plugin_options.
+ *
+ * @param string $post_type Type of the post.
+ */
+ private function set_valid_plugin_options( $post_type = 'post' ): void {
+ TestCase::set_options(
+ array(
+ 'apikey' => 'test',
+ 'api_secret' => 'test',
+ 'track_post_types' => array( $post_type ),
+ )
+ );
+ }
+
+ /**
+ * Sets valid conditions under which we add hooks for Parse.ly Stats.
+ *
+ * @param string $post_type Type of the post.
+ */
+ private function set_valid_conditions_for_parsely_stats( $post_type = 'post' ): void {
+ $this->set_valid_plugin_options( $post_type );
+ set_current_screen( "edit-$post_type" );
+ }
+}
diff --git a/tests/Integration/UI/AdminWarningTest.php b/tests/Integration/UI/AdminWarningTest.php
index 713b7e55d..131108898 100644
--- a/tests/Integration/UI/AdminWarningTest.php
+++ b/tests/Integration/UI/AdminWarningTest.php
@@ -41,8 +41,8 @@ public function set_up(): void {
*
* @covers \Parsely\UI\Admin_Warning::should_display_admin_warning
* @covers \Parsely\UI\Admin_Warning::__construct
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
public function test_display_admin_warning_without_key(): void {
@@ -51,7 +51,7 @@ public function test_display_admin_warning_without_key(): void {
}
$should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class );
- $this->set_options( array( 'apikey' => '' ) );
+ self::set_options( array( 'apikey' => '' ) );
$response = $should_display_admin_warning->invoke( self::$admin_warning );
self::assertTrue( $response );
@@ -63,13 +63,13 @@ public function test_display_admin_warning_without_key(): void {
*
* @covers \Parsely\UI\Admin_Warning::should_display_admin_warning
* @covers \Parsely\UI\Admin_Warning::__construct
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
public function test_display_admin_warning_without_key_old_wp(): void {
$should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class );
- $this->set_options( array( 'apikey' => '' ) );
+ self::set_options( array( 'apikey' => '' ) );
set_current_screen( 'settings_page_parsely' );
$response = $should_display_admin_warning->invoke( self::$admin_warning );
@@ -85,7 +85,7 @@ public function test_display_admin_warning_without_key_old_wp(): void {
*/
public function test_display_admin_warning_network_admin(): void {
$should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class );
- $this->set_options( array( 'apikey' => '' ) );
+ self::set_options( array( 'apikey' => '' ) );
set_current_screen( 'dashboard-network' );
$response = $should_display_admin_warning->invoke( self::$admin_warning );
@@ -98,13 +98,13 @@ public function test_display_admin_warning_network_admin(): void {
*
* @covers \Parsely\UI\Admin_Warning::should_display_admin_warning
* @covers \Parsely\UI\Admin_Warning::__construct
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::get_options
*/
public function test_display_admin_warning_with_key(): void {
$should_display_admin_warning = self::get_method( 'should_display_admin_warning', Admin_Warning::class );
- $this->set_options( array( 'apikey' => 'somekey' ) );
+ self::set_options( array( 'apikey' => 'somekey' ) );
$response = $should_display_admin_warning->invoke( self::$admin_warning );
self::assertFalse( $response );
diff --git a/tests/Integration/UI/MetadataRendererTest.php b/tests/Integration/UI/MetadataRendererTest.php
index 00b242da6..241fdae5a 100644
--- a/tests/Integration/UI/MetadataRendererTest.php
+++ b/tests/Integration/UI/MetadataRendererTest.php
@@ -100,8 +100,8 @@ public function test_run_wp_head_action_with_filter(): void {
* @uses \Parsely\Metadata\Post_Builder::get_coauthor_names
* @uses \Parsely\Metadata\Post_Builder::get_metadata
* @uses \Parsely\Metadata\Post_Builder::get_tags
- * @uses \Parsely\Parsely::api_key_is_missing
- * @uses \Parsely\Parsely::api_key_is_set
+ * @uses \Parsely\Parsely::site_id_is_missing
+ * @uses \Parsely\Parsely::site_id_is_set
* @uses \Parsely\Parsely::convert_jsonld_to_parsely_type
* @uses \Parsely\Parsely::get_options
* @uses \Parsely\Parsely::post_has_trackable_status
@@ -115,6 +115,11 @@ public function test_render_metadata_json_ld(): void {
ob_start();
self::$metadata_renderer->render_metadata( 'json_ld' );
+ /**
+ * Variable.
+ *
+ * @var string
+ */
$out = ob_get_clean();
self::assertStringContainsString( '` );
expect( content ).toContain( `` );
expect( content ).not.toContain( "` );
expect( content ).toContain( `` );
expect( content ).toContain( '