diff --git a/.changeset/afraid-dolphins-joke.md b/.changeset/afraid-dolphins-joke.md new file mode 100644 index 0000000..fd1d7c8 --- /dev/null +++ b/.changeset/afraid-dolphins-joke.md @@ -0,0 +1,8 @@ +--- +"wpgraphql-ide": patch +--- + +- fix: The IDE no longer waits on `DOMContentLoaded` in order to help client side performance with heavier pages. +- add: New PHP filters for updating the drawer label: + - `wpgraphqlide_drawer_button_label` + - `wpgraphqlide_drawer_button_loading_label` diff --git a/package-lock.json b/package-lock.json index f6181de..4fc69e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,12 @@ { "name": "wpgraphql-ide", + "version": "1.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "wpgraphql-ide", + "version": "1.1.8", "dependencies": { "@changesets/cli": "^2.27.1", "@graphiql/plugin-explorer": "^1.0.3", diff --git a/src/App.jsx b/src/App.jsx index 4a751be..8ed2ec2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,5 @@ /* global WPGRAPHQL_IDE_DATA */ -import { createRoot, useEffect } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; import { doAction } from '@wordpress/hooks'; import { useDispatch, useSelect } from '@wordpress/data'; import { parse, print } from 'graphql'; @@ -8,7 +8,10 @@ import LZString from 'lz-string'; import { EditorDrawer } from './components/EditorDrawer'; import { Editor } from './components/Editor'; -const { isDedicatedIdePage } = window.WPGRAPHQL_IDE_DATA; +const { + isDedicatedIdePage, + context: { drawerButtonLabel }, +} = window.WPGRAPHQL_IDE_DATA; const url = new URL( window.location.href ); const params = url.searchParams; @@ -129,7 +132,7 @@ export function RenderApp() { return (
- +
diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 872dee9..e99d9fb 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -3,7 +3,7 @@ import { GraphiQL } from 'graphiql'; import { useDispatch, useSelect } from '@wordpress/data'; import { parse, visit } from 'graphql'; import { explorerPlugin } from '@graphiql/plugin-explorer'; -import { helpPlugin } from './help' +import { helpPlugin } from './help'; import { PrettifyButton } from './toolbarButtons/PrettifyButton'; import { CopyQueryButton } from './toolbarButtons/CopyQueryButton'; @@ -11,7 +11,6 @@ import { MergeFragmentsButton } from './toolbarButtons/MergeFragmentsButton'; import { ShareDocumentButton } from './toolbarButtons/ShareDocumentButton'; import { ToggleAuthButton } from './toolbarButtons/ToggleAuthButton'; - import 'graphiql/graphiql.min.css'; /** @@ -129,7 +128,7 @@ export function Editor() { setSchema( newSchema ); } } } - plugins={[ explorer, help ]} + plugins={ [ explorer, help ] } > { return select( 'wpgraphql-ide' ).isDrawerOpen(); } ); diff --git a/src/components/help/index.jsx b/src/components/help/index.jsx index f4e441a..741d0b8 100644 --- a/src/components/help/index.jsx +++ b/src/components/help/index.jsx @@ -6,12 +6,12 @@ export const helpPlugin = () => { title: 'Help', icon: () => ( ), - content: () => , + content: () => , }; }; diff --git a/src/components/toolbarButtons/ToggleAuthButton.jsx b/src/components/toolbarButtons/ToggleAuthButton.jsx index fd27bca..18eb7ef 100644 --- a/src/components/toolbarButtons/ToggleAuthButton.jsx +++ b/src/components/toolbarButtons/ToggleAuthButton.jsx @@ -22,11 +22,15 @@ export const ToggleAuthButton = ( { return ( diff --git a/src/index.js b/src/index.js index ddea706..5cc3e79 100644 --- a/src/index.js +++ b/src/index.js @@ -1,37 +1,49 @@ -// WordPress dependencies for hooks, data handling, and component rendering. +/** + * @file + * Initializes the WPGraphQL IDE by setting up the necessary WordPress hooks, + * registering the global store, and exposing the GraphQL functionality through a global IDE object. + */ + +// External dependencies import { createHooks } from '@wordpress/hooks'; import { register } from '@wordpress/data'; import { createRoot } from '@wordpress/element'; -import * as GraphQL from "graphql/index.js"; +import * as GraphQL from 'graphql/index.js'; -// Local imports including the store configuration and the main App component. +// Internal dependencies import { store } from './store'; import { App } from './App'; -// Register the store with wp.data to make it available throughout the plugin. +/** + * Registers the application's data store with WordPress to enable global state management. + */ register( store ); -// Create a central event hook system for the WPGraphQL IDE. +/** + * Initializes a hooks system to extend the functionality of the WPGraphQL IDE through + * external scripts and internal plugin hooks. + */ export const hooks = createHooks(); -// Expose a global variable for the IDE, facilitating extension through external scripts. +/** + * Exposes a global `WPGraphQLIDE` variable that includes hooks, store, and GraphQL references, + * making them accessible for extensions and external scripts. + */ window.WPGraphQLIDE = { hooks, store, - GraphQL + GraphQL, }; /** - * Initialize and render the application once the DOM is fully loaded. - * This ensures that the application mounts when all page elements are available. + * Attempts to render the React application to a specified mount point in the DOM. + * Logs an error to the console if the mount point is missing. */ -document.addEventListener( 'DOMContentLoaded', () => { - const appMountPoint = document.getElementById( 'wpgraphql-ide-root' ); - if ( appMountPoint ) { - createRoot( appMountPoint ).render( ); - } else { - console.error( - 'WPGraphQL IDE mount point not found. Please ensure an element with ID "wpgraphql-ide-root" exists.' - ); - } -} ); +const appMountPoint = document.getElementById( 'wpgraphql-ide-root' ); +if ( appMountPoint ) { + createRoot( appMountPoint ).render( ); +} else { + console.error( + 'WPGraphQL IDE mount point not found. Please ensure an element with ID "wpgraphql-ide-root" exists.' + ); +} diff --git a/tests/e2e/specs/drawer.spec.js b/tests/e2e/specs/drawer.spec.js index 3a0b53f..4d65d04 100644 --- a/tests/e2e/specs/drawer.spec.js +++ b/tests/e2e/specs/drawer.spec.js @@ -2,6 +2,7 @@ import { loginToWordPressAdmin, openDrawer, pasteVariables, + simulateHeavyJSLoad, typeQuery, typeVariables, visitAdminFacingPage, @@ -37,6 +38,31 @@ test( 'should open and close successfully', async ( { page } ) => { ).not.toBeVisible(); } ); +test( 'should open on JS-heavy admin page with CPU Throttling', async ( { page } ) => { + await visitAdminFacingPage( page ); + + // Start a new CDP Session to control the browser + const context = page.context(); + const session = await context.newCDPSession( page ); + + // Set CPU Throttling + await session.send( 'Emulation.setCPUThrottlingRate', { rate: 4 }); // Throttles CPU to 1/4th its speed + + // Now simulate heavy JavaScript load + await simulateHeavyJSLoad( page ); + + // Check if the drawer is initially hidden + await expect( page.locator( '.graphiql-container' ) ).toBeHidden(); + + // Open the drawer and ensure it still functions under throttled conditions + await openDrawer( page ); + await expect( page.locator( '.graphiql-container' ) ).toBeVisible(); + + // Optionally, reset CPU throttling rate after test + await session.send( 'Emulation.setCPUThrottlingRate', { rate: 1 } ); +}); + + test( 'should execute a GraphQL query successfully', async ( { page } ) => { await page.click( selectors.editorDrawerButton ); await expect( page.locator( selectors.graphiqlContainer ) ).toBeVisible(); diff --git a/tests/e2e/specs/environment.spec.js b/tests/e2e/specs/environment.spec.js new file mode 100644 index 0000000..699ff3a --- /dev/null +++ b/tests/e2e/specs/environment.spec.js @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; +import { loginToWordPressAdmin, visitPluginsPage } from '../utils'; + +test.beforeEach(async ({ page }) => { + await loginToWordPressAdmin(page); +}); + +test( 'expect the enqueued wpgraphql-ide script to have the defer attribute', async ( { page } ) => { + await visitPluginsPage( page ); + + // Retrieve the script tags and check for the defer attribute + const hasDeferAttribute = await page.evaluate( () => { + const scripts = Array.from( document.querySelectorAll( 'script' ) ); + const targetScript = scripts.find( script => script.src.includes( 'wpgraphql-ide' ) ); + return targetScript?.hasAttribute( 'defer' ); + }); + + // Assert that the defer attribute is present + expect( hasDeferAttribute ).toBe( true ); +}); diff --git a/tests/e2e/utils.js b/tests/e2e/utils.js index 79dacfa..982fce5 100644 --- a/tests/e2e/utils.js +++ b/tests/e2e/utils.js @@ -186,3 +186,30 @@ async function selectAndClearTextUsingKeyboard( page, selector ) { await page.keyboard.press( selectAllCommand ); // Select all text using OS-specific shortcut await page.keyboard.press( 'Backspace' ); // Clear selected text } + +export async function simulateHeavyJSLoad(page) { + await page.evaluate(() => { + // Simulate heavy DOM manipulations + for (let i = 0; i < 500; i++) { + const div = document.createElement('div'); + div.textContent = `Heavy content ${i}`; + div.style.backgroundColor = "#" + Math.floor(Math.random()*16777215).toString(16); + document.body.appendChild(div); + } + + // Simulate heavy computations + const heavyComputation = Array.from({ length: 50000 }, (_, i) => i ** 2).reduce((a, b) => a + b); + console.log('Heavy computation result:', heavyComputation); + + // Simulate asynchronous operations + new Promise(resolve => setTimeout(resolve, 5000)).then(() => console.log('Delayed operation completed')); + + // Simulate frequent DOM updates + setInterval(() => { + const randomDiv = document.querySelector(`div:nth-child(${Math.floor(Math.random() * 500) + 1})`); + if (randomDiv) { + randomDiv.style.backgroundColor = "#" + Math.floor(Math.random()*16777215).toString(16); + } + }, 10); + }); +} \ No newline at end of file diff --git a/wpgraphql-ide.php b/wpgraphql-ide.php index a29b62c..66ecc38 100644 --- a/wpgraphql-ide.php +++ b/wpgraphql-ide.php @@ -20,7 +20,7 @@ exit; } -define( 'WPGRAPHQL_IDE_VERSION', '1.0.1' ); +define( 'WPGRAPHQL_IDE_VERSION', '1.1.8' ); define( 'WPGRAPHQL_IDE_ROOT_ELEMENT_ID', 'wpgraphql-ide-root' ); define( 'WPGRAPHQL_IDE_PLUGIN_DIR_PATH', plugin_dir_path( __FILE__ ) ); define( 'WPGRAPHQL_IDE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); @@ -117,6 +117,8 @@ function register_wpadminbar_menus(): void { global $wp_admin_bar; + $app_context = get_app_context(); + // Link to the new dedicated IDE page. $wp_admin_bar->add_node( [ @@ -131,7 +133,7 @@ function register_wpadminbar_menus(): void { $wp_admin_bar->add_node( [ 'id' => 'wpgraphql-ide-button', - 'title' => '
', + 'title' => '
' . $app_context['drawerButtonLoadingLabel'] . '
', 'href' => '#', ] ); @@ -234,7 +236,7 @@ function enqueue_react_app_with_styles(): void { plugins_url( 'build/index.js', __FILE__ ), $asset_file['dependencies'], $asset_file['version'], - true + false ); wp_localize_script( @@ -305,10 +307,12 @@ function get_app_context(): array { return apply_filters( 'wpgraphqlide_context', [ - 'pluginVersion' => get_plugin_header( 'Version' ), - 'pluginName' => get_plugin_header( 'Name' ), - 'externalFragments' => apply_filters( 'wpgraphqlide_external_fragments', [] ), - 'avatarUrl' => $avatar_url, + 'pluginVersion' => get_plugin_header( 'Version' ), + 'pluginName' => get_plugin_header( 'Name' ), + 'externalFragments' => apply_filters( 'wpgraphqlide_external_fragments', [] ), + 'avatarUrl' => $avatar_url, + 'drawerButtonLabel' => apply_filters( 'wpgraphqlide_drawer_button_label', '🚀' ), + 'drawerButtonLoadingLabel' => apply_filters( 'wpgraphqlide_drawer_button_loading_label', '⏳' ), ] ); } @@ -391,3 +395,29 @@ static function ( bool $is_plugin_scoped_page, string $current_page_id, array $a 10, 3 ); + +/** + * Modifies the script tag for specific scripts to add the 'defer' attribute. + * + * This function checks if the script handle matches 'wpgraphql-ide' and, if so, + * adds the 'defer' attribute to the script tag. This allows the script to be executed + * after the HTML document is parsed but before the DOMContentLoaded event. + * + * @param string $tag The HTML `