Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/css/common/_toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ $wpcontent-inline-start-indent: 20px;
a {
color: inherit;
text-decoration: none;
position: relative;
}

.whats-new-dot {
position: absolute;
inset-block-start: -5px;
inset-inline-end: -10px;
inline-size: 8px;
block-size: 8px;
border-radius: 50%;
background: #d63638;
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/js/components/common/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface NavLink {
pageSlug?: string
subpage?: typeof SUBPAGES[number]
end?: boolean
whatsNewUnseen?: boolean
}

const UPPER_NAV_LINKS: readonly NavLink[] = [
Expand All @@ -38,7 +39,8 @@ const UPPER_NAV_LINKS: readonly NavLink[] = [
name: 'welcome',
url: window.CODE_SNIPPETS?.urls.welcome,
label: __("What's New", 'code-snippets'),
pageSlug: 'code-snippets-welcome'
pageSlug: 'code-snippets-welcome',
whatsNewUnseen: window.CODE_SNIPPETS?.whatsNewUnseen
}
]

Expand Down Expand Up @@ -106,6 +108,13 @@ const UpperNav: React.FC<NavProps> = ({ setIsUpsellDialogOpen }) =>
{...link.external && { target: '_blank', rel: 'noopener noreferrer' }}
>
{link.label}
{link.whatsNewUnseen && (
<span className="whats-new-dot">
<span className="screen-reader-text">
{__('New content available', 'code-snippets')}
</span>
</span>
)}
</a>
</li>)}

Expand Down
1 change: 1 addition & 0 deletions src/js/types/Window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ declare global {
isLicensed: boolean
hideUpsell: boolean
isCloudConnected: boolean
whatsNewUnseen: boolean
snippetView: SnippetView
restAPI: {
base: string
Expand Down
11 changes: 11 additions & 0 deletions src/php/Admin/Menus/Welcome_Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Code_Snippets\Admin\Menus;

use Code_Snippets\Admin\Whats_New_Badge;
use Code_Snippets\Client\Welcome_Client;
use function Code_Snippets\code_snippets;
use const Code_Snippets\PLUGIN_FILE;
Expand Down Expand Up @@ -42,6 +43,16 @@ public function __construct( $client ) {
*
* @return void
*/
public function load() {
parent::load();
Whats_New_Badge::mark_seen_release();
}

/**
* Render the welcome menu.
*
* @return void
*/
public function render() {
echo '<div id="code-snippets-welcome-container" class="wrap"></div>';
}
Expand Down
59 changes: 59 additions & 0 deletions src/php/Admin/Whats_New_Badge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Code_Snippets\Admin;

use const Code_Snippets\PLUGIN_VERSION;

/**
* Tracks whether the current user has seen the latest What's New page.
*/
class Whats_New_Badge {

/**
* User meta key for the latest seen plugin version.
*/
public const USER_META_KEY = 'code_snippets_whats_new_seen_version';

/**
* Determine whether the current release is unseen.
*
* @return bool
*/
public static function has_unseen_release(): bool {
$user_id = get_current_user_id();

if ( ! $user_id ) {
return false;
}

$seen_version = get_user_meta( $user_id, self::USER_META_KEY, true );

return ! is_string( $seen_version ) ||
'' === $seen_version ||
version_compare( PLUGIN_VERSION, $seen_version, '>' );
}

/**
* Mark the current release as seen.
*
* @return void
*/
public static function mark_seen_release(): void {
$user_id = get_current_user_id();

if ( $user_id ) {
update_user_meta( $user_id, self::USER_META_KEY, PLUGIN_VERSION );
}
}

/**
* Prevent the indicator from appearing immediately after a fresh install.
*
* @return void
*/
public static function seed_fresh_install(): void {
if ( false === get_option( 'code_snippets_version' ) ) {
self::mark_seen_release();
}
}
}
3 changes: 3 additions & 0 deletions src/php/Core/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Code_Snippets;

use Code_Snippets\Admin\Whats_New_Badge;
use Composer\Autoload\ClassLoader;

/**
Expand Down Expand Up @@ -85,4 +86,6 @@ function code_snippets(): Plugin {
return $plugin;
}

register_activation_hook( PLUGIN_FILE, [ Whats_New_Badge::class, 'seed_fresh_install' ] );

code_snippets()->load_plugin();
2 changes: 2 additions & 0 deletions src/php/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Code_Snippets;

use Code_Snippets\Admin\Bootstrap_Admin;
use Code_Snippets\Admin\Whats_New_Badge;
use Code_Snippets\Controller\Cloud_Search_Controller;
use Code_Snippets\Core\DB;
use Code_Snippets\Core\Licensing;
Expand Down Expand Up @@ -348,6 +349,7 @@ public function localize_script( string $handle ) {
'isLicensed' => $this->licensing->is_licensed(),
'isCloudConnected' => $this->cloud_connection->is_authenticated(),
'hideUpsell' => Settings\get_setting( 'general', 'hide_upgrade_menu' ),
'whatsNewUnseen' => Whats_New_Badge::has_unseen_release(),
'snippetView' => Preferences_REST_Controller::get_snippet_view(),
'restAPI' => [
'base' => esc_url_raw( rest_url() ),
Expand Down
33 changes: 33 additions & 0 deletions tests/e2e/code-snippets-whats-new.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, test } from '@playwright/test'
import { URLS } from './helpers/constants'
import { wpCli } from './helpers/wpCli'

const WHATS_NEW_SEEN_META_KEY = 'code_snippets_whats_new_seen_version'

test.describe("What's New unseen release indicator", () => {
test.afterEach(async () => {
await wpCli(['user', 'meta', 'delete', 'admin', WHATS_NEW_SEEN_META_KEY])
})

test('Dot clears after opening the page and stays cleared', async ({ page }) => {
await wpCli(['user', 'meta', 'update', 'admin', WHATS_NEW_SEEN_META_KEY, '1.0.0'])
await page.goto(URLS.SNIPPETS_ADMIN)

const upperNav = page.locator('.code-snippets-toolbar-upper')
const whatsNewLink = upperNav.getByRole('link', { name: /What's New/ })
await expect(whatsNewLink.locator('.whats-new-dot')).toBeVisible()
await expect(whatsNewLink).toContainText('New content available')

await whatsNewLink.click()
await expect(page).toHaveURL(URLS.WELCOME_SCREEN_ADMIN)
await expect(upperNav.getByRole('link', { name: /What's New/ }).locator('.whats-new-dot')).toBeHidden()

const currentVersion = (await wpCli(['eval', 'echo CODE_SNIPPETS_VERSION;'])).trim()
const seenVersion =
(await wpCli(['user', 'meta', 'get', 'admin', WHATS_NEW_SEEN_META_KEY])).trim()
expect(seenVersion).toBe(currentVersion)

await page.goto(URLS.SNIPPETS_ADMIN)
await expect(upperNav.getByRole('link', { name: /What's New/ }).locator('.whats-new-dot')).toBeHidden()
})
})
56 changes: 56 additions & 0 deletions tests/unit/Admin/Menus/Welcome_Menu_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Code_Snippets\Admin\Menus;

use Code_Snippets\Admin\Whats_New_Badge;
use Code_Snippets\Client\Welcome_Client;
use Code_Snippets\UnitTestCase;
use WP_UnitTest_Factory;
use function Code_Snippets\code_snippets;
use const Code_Snippets\PLUGIN_VERSION;

/**
* Tests for the What's New menu.
*/
class Welcome_Menu_Test extends UnitTestCase {

/**
* Administrator user ID.
*
* @var int
*/
protected static int $admin_user_id;

/**
* Create the administrator fixture.
*
* @param WP_UnitTest_Factory $factory Factory object.
*
* @return void
*/
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$admin_user_id = $factory->user->create( [ 'role' => 'administrator' ] );
}

/**
* Loading the page marks the current release seen before assets enqueue.
*
* @return void
*/
public function test_load_marks_current_release_seen(): void {
wp_set_current_user( self::$admin_user_id );
set_current_screen( 'snippets_page_' . code_snippets()->get_menu_slug( 'welcome' ) );
update_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, '1.0.0' );

$client = $this->getMockBuilder( Welcome_Client::class )
->disableOriginalConstructor()
->getMock();
$menu = new Welcome_Menu( $client );
$menu->load();

$this->assertSame(
PLUGIN_VERSION,
get_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, true )
);
}
}
112 changes: 112 additions & 0 deletions tests/unit/Admin/Whats_New_Badge_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Code_Snippets\Admin;

use Code_Snippets\UnitTestCase;
use WP_UnitTest_Factory;
use const Code_Snippets\PLUGIN_VERSION;

/**
* Tests for the What's New unseen-release state.
*/
class Whats_New_Badge_Test extends UnitTestCase {

/**
* Administrator user ID.
*
* @var int
*/
protected static int $admin_user_id;

/**
* Create the administrator fixture.
*
* @param WP_UnitTest_Factory $factory Factory object.
*
* @return void
*/
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$admin_user_id = $factory->user->create( [ 'role' => 'administrator' ] );
}

/**
* Reset the current user's seen version.
*
* @return void
*/
public function set_up() {
parent::set_up();
wp_set_current_user( self::$admin_user_id );
delete_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY );
}

/**
* Check whether stored versions identify an unseen release.
*
* @dataProvider provide_seen_versions
*
* @param mixed $seen Stored user-meta value.
* @param bool $expected Whether the current release is unseen.
*
* @return void
*/
public function test_has_unseen_release( $seen, bool $expected ): void {
if ( null !== $seen ) {
update_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, $seen );
}

$this->assertSame( $expected, Whats_New_Badge::has_unseen_release() );
}

/**
* Provide stored seen versions and their expected state.
*
* @return array<string, array{mixed, bool}>
*/
public static function provide_seen_versions(): array {
return [
'missing' => [ null, true ],
'older' => [ '1.0.0', true ],
'current' => [ PLUGIN_VERSION, false ],
'newer' => [ '99.0.0', false ],
'malformed array' => [ [ 'unexpected' ], true ],
'malformed string' => [ 'garbage', true ],
];
}

/**
* Marking the page seen stores the current release for this user.
*
* @return void
*/
public function test_mark_seen_release(): void {
Whats_New_Badge::mark_seen_release();

$this->assertSame(
PLUGIN_VERSION,
get_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, true )
);
}

/**
* Activation seeds fresh installs without overwriting reactivations.
*
* @return void
*/
public function test_seed_fresh_install(): void {
delete_option( 'code_snippets_version' );
Whats_New_Badge::seed_fresh_install();
$this->assertSame(
PLUGIN_VERSION,
get_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, true )
);

update_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, '1.0.0' );
update_option( 'code_snippets_version', PLUGIN_VERSION );
Whats_New_Badge::seed_fresh_install();
$this->assertSame(
'1.0.0',
get_user_meta( self::$admin_user_id, Whats_New_Badge::USER_META_KEY, true )
);
}
}
Loading
Loading