Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .github/workflows/phpunit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
if [ -f "src/composer.lock" ]; then
cat "src/composer.lock" >> "$tmpfile"
fi
if [ -f "src/composer.json" ]; then
cat "src/composer.json" >> "$tmpfile"
fi
if [ -s "$tmpfile" ]; then
deps_hash=$(shasum -a 1 "$tmpfile" | awk '{print $1}' | cut -c1-8)
else
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ Thumbs.db
.tmp
tmp
.continue
.codex
.codex
# Local environment overrides (license keys, secrets)
/.env
7 changes: 5 additions & 2 deletions config/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { defineConfig, devices } from '@playwright/test'

const WORKERS = 1

const CI_RETRIES = 2
const LOCAL_RETRIES = 1

const TEST_TIMEOUT_SECONDS = 60
const ASSERT_TIMEOUT_SECONDS = 30

Expand All @@ -20,8 +23,8 @@ export default defineConfig({
snapshotPathTemplate: '{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-{platform}{ext}',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
workers: process.env.CI ? WORKERS : WORKERS,
retries: process.env.CI ? CI_RETRIES : LOCAL_RETRIES,
workers: WORKERS,
reporter: process.env.CI
? [
['line'],
Expand Down
4 changes: 2 additions & 2 deletions config/webpack/webpack-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const jsWebpackConfig: Configuration = {
'welcome': `${SOURCE_DIR}/welcome.ts`
},
output: {
path: join(resolve(__dirname), '..', DEST_DIR),
path: join(resolve(__dirname), '..', '..', DEST_DIR),
filename: '[name].js',
clean: true
},
Expand All @@ -58,7 +58,7 @@ export const jsWebpackConfig: Configuration = {
)
},
resolve: {
modules: [resolve(__dirname, '..', 'node_modules')],
modules: [resolve(__dirname, '..', '..', 'node_modules')],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']
},
module: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"scripts": {
"prepare": "husky install",
"test:php": "WP_TESTS_DIR=./.wp-tests-lib WP_DEVELOP_DIR=./.wp-core src/vendor/bin/phpunit -c phpunit.xml",
"test:php": "WP_TESTS_DIR=./.wp-tests-lib src/vendor/bin/phpunit -c phpunit.xml",
"test:php:watch": "npm run test:php -- --testdox",
"test:playwright": "playwright test -c config/playwright/playwright.config.ts",
"test:playwright:debug": "npm run test:playwright -- --debug",
Expand Down
16 changes: 15 additions & 1 deletion scripts/test-setup-playwright.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
#!/usr/bin/env ts-node

import { execFileSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'

const run = (cmd: string, args: readonly string[]) => {
execFileSync(cmd, args, { stdio: 'inherit' })
}

const runWpEnvCli = (args: readonly string[]) => run('npx', ['wp-env', 'run', 'cli', ...args])

const getPluginSlug = (): string => {
const prefix = 'wp-content/plugins/'
const config = <{ mappings?: Record<string, string> }>JSON.parse(readFileSync(resolve(process.cwd(), '.wp-env.json'), 'utf8'))
const mapping = Object.keys(config.mappings ?? {}).find(key => key.startsWith(prefix))

if (!mapping) {
throw new Error('No plugin mapping found in .wp-env.json')
}

return mapping.slice(prefix.length)
}

const main = () => {
// Ensure a clean slate for file-based execution tests:
// - remove flat-file execution directory (stale indexes can break the WP site)
Expand All @@ -16,7 +30,7 @@ const main = () => {
// - delete all DB snippets with an E2E prefix (keeps list clean across runs)

runWpEnvCli(['sh', '-lc', 'rm -rf wp-content/code-snippets'])
runWpEnvCli(['wp', 'plugin', 'activate', 'code-snippets'])
runWpEnvCli(['wp', 'plugin', 'activate', getPluginSlug()])

runWpEnvCli([
'wp',
Expand Down
8 changes: 5 additions & 3 deletions src/php/Admin/Menus/Manage_Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,8 @@ public function handle_bulk_download_request(): void {
$this->send_download_error( __( 'The download request is no longer valid. Please refresh and try again.', 'code-snippets' ), 403 );
}

$snippets = $this->get_requested_download_snippets();
$snippets_json = wp_unslash( filter_input( INPUT_POST, 'snippets', FILTER_DEFAULT ) ?? '' );
$snippets = $this->get_requested_download_snippets( $snippets_json );

if ( $snippets instanceof WP_Error ) {
$status = $snippets->get_error_data( 'status' );
Expand Down Expand Up @@ -505,10 +506,11 @@ private function is_bulk_download_request(): bool {
/**
* Resolve the snippets requested for download.
*
* @param string $snippets_json JSON-encoded list of requested snippets.
*
* @return Snippet[]|WP_Error
*/
private function get_requested_download_snippets() {
$snippets_json = wp_unslash( filter_input( INPUT_POST, 'snippets', FILTER_DEFAULT ) ?? '' );
private function get_requested_download_snippets( string $snippets_json ) {
$payload = '' === $snippets_json ? [] : json_decode( $snippets_json, true );

if ( ! is_array( $payload ) ) {
Expand Down
2 changes: 1 addition & 1 deletion src/php/REST_API/Snippets/Snippets_REST_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private function is_network_scoped_request( $request ): bool {
}

if ( is_string( $network ) ) {
return in_array( strtolower( $network ), [ '1', 'true', 'yes' ], true );
return ! in_array( strtolower( $network ), [ '0', 'false', 'no', '' ], true );
}

return (bool) $network;
Expand Down
2 changes: 1 addition & 1 deletion src/php/snippet-ops.php
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ function save_snippet( $snippet ): ?Snippet {
}

$snippet->id = $wpdb->insert_id;
$updated = get_snippet( $snippet->id );
$updated = get_snippet( $snippet->id, $snippet->network );
$updated->code_error = $snippet->code_error;
$updated->code_error_trace = $snippet->code_error_trace;
do_action( 'code_snippets/create_snippet', $updated, $table );
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ npm run test:php:watch
Or run PHPUnit directly:

```bash
WP_TESTS_DIR=./.wp-tests-lib WP_DEVELOP_DIR=./.wp-core src/vendor/bin/phpunit -c phpunit.xml
WP_TESTS_DIR=./.wp-tests-lib src/vendor/bin/phpunit -c phpunit.xml
```

## What Gets Installed
Expand Down
3 changes: 0 additions & 3 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ function _get_tests_dir(): string {
case (bool) getenv( 'WP_TESTS_DIR' ):
return getenv( 'WP_TESTS_DIR' );

case (bool) getenv( 'WP_DEVELOP_DIR' ):
return getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit';

case (bool) getenv( 'WP_PHPUNIT__DIR' ):
return getenv( 'WP_PHPUNIT__DIR' );

Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/code-snippets-edit.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { expect, test } from '@playwright/test'
import { SnippetsTestHelper } from './helpers/SnippetsTestHelper'
import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper'
import { MESSAGES, SELECTORS, TIMEOUTS } from './helpers/constants'

test.describe('Code Snippets Admin', () => {
let helper: SnippetsTestHelper

test.beforeEach(async ({ page }) => {
helper = new SnippetsTestHelper(page)
await SnippetsTestHelper.cleanupSnippetsByPrefix(DEFAULT_E2E_SNIPPET_BASE_NAME)
await helper.navigateToSnippetsAdmin()
})

Expand Down
16 changes: 2 additions & 14 deletions tests/e2e/code-snippets-evaluation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,9 @@ test.describe('Code Snippets Evaluation', () => {
test.beforeEach(async ({ page }) => {
helper = new SnippetsTestHelper(page)
snippetName = SnippetsTestHelper.makeUniqueSnippetName()
await helper.navigateToSnippetsAdmin()

// Ensure isolation: file-based execution runs from flat files, so we must delete old snippets
// via plugin operations (to keep flat files in sync), not via direct SQL.
await wpCli([
'eval',
`
global $wpdb;
$table = $wpdb->prefix . 'snippets';
$ids = $wpdb->get_col(
$wpdb->prepare( "SELECT id FROM {$table} WHERE name LIKE %s", "${DEFAULT_E2E_SNIPPET_BASE_NAME}%" )
);
foreach ( $ids as $id ) { \\Code_Snippets\\delete_snippet( intval( $id ), false ); }
`
])
await SnippetsTestHelper.cleanupSnippetsByPrefix(DEFAULT_E2E_SNIPPET_BASE_NAME)
await helper.navigateToSnippetsAdmin()
})

test('PHP snippet is evaluating correctly', async () => {
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/code-snippets-list.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync } from 'fs'
import { expect, test } from '@playwright/test'
import { SnippetsTestHelper } from './helpers/SnippetsTestHelper'
import { DEFAULT_E2E_SNIPPET_BASE_NAME, SnippetsTestHelper } from './helpers/SnippetsTestHelper'
import { SELECTORS } from './helpers/constants'
import type { Page } from '@playwright/test'

Expand All @@ -12,6 +12,7 @@ test.describe('Code Snippets List Page Actions', () => {
test.beforeEach(async ({ page }) => {
helper = new SnippetsTestHelper(page)
snippetName = SnippetsTestHelper.makeUniqueSnippetName()
await SnippetsTestHelper.cleanupSnippetsByPrefix(DEFAULT_E2E_SNIPPET_BASE_NAME)
await helper.navigateToSnippetsAdmin()

await helper.createAndActivateSnippet({
Expand Down Expand Up @@ -325,6 +326,7 @@ test.describe('Manage table Screen Options', () => {
test.beforeEach(async ({ page }) => {
helper = new SnippetsTestHelper(page)
snippetName = SnippetsTestHelper.makeUniqueSnippetName('E2E Screen Options')
await SnippetsTestHelper.cleanupSnippetsByPrefix(DEFAULT_E2E_SNIPPET_BASE_NAME)
await helper.createAndActivateSnippet({
name: snippetName,
code: "add_filter('show_admin_bar', '__return_false');"
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/code-snippets-quicknav-admin-bar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test.describe('Admin Bar Snippets QuickNav', () => {
let inactiveA: string

test.beforeAll(async () => {
test.setTimeout(QUICKNAV_TEST_TIMEOUT_MS)
await SnippetsTestHelper.setAdminBarQuickNavSettings({ enabled: true, perPage: QUICKNAV_PER_PAGE })
await SnippetsTestHelper.cleanupSnippetsByPrefix(QUICKNAV_PREFIX)

Expand Down
32 changes: 28 additions & 4 deletions tests/e2e/helpers/SnippetsTestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const RANDOM_RADIX = 36
const RANDOM_SLICE_START = 2
const RANDOM_SLICE_END = 7
const CLICK_RETRIES = 3
const SAVE_CONFIRM_RETRIES = 3
const AT_LEAST_ONE = 1

const getErrorMessage = (error: unknown): string => {
Expand Down Expand Up @@ -288,7 +289,7 @@ export class SnippetsTestHelper {
if ('save_and_activate' === action) {
const activateButton = this.page.locator(BUTTONS.SAVE_AND_ACTIVATE).first()
if (await activateButton.isVisible().catch(() => false)) {
await this.clickButton(/^Save and Activate$/i)
await this.clickSaveAndConfirm(/^Save and Activate$/i)
return
}

Expand All @@ -297,7 +298,7 @@ export class SnippetsTestHelper {
if (await inactiveToggle.isVisible().catch(() => false)) {
await inactiveToggle.click({ timeout: TIMEOUTS.DEFAULT, force: true })
}
await this.clickButton(/^Save Snippet$/i)
await this.clickSaveAndConfirm(/^Save Snippet$/i)
return
}

Expand All @@ -312,11 +313,34 @@ export class SnippetsTestHelper {
await statusToggle.click({ timeout: TIMEOUTS.DEFAULT, force: true })
}
}
await this.clickButton(/^Save Snippet$/i)
await this.clickSaveAndConfirm(/^Save Snippet$/i)
return
}

await this.clickButton(/^Save Snippet$/i)
await this.clickSaveAndConfirm(/^Save Snippet$/i)
}

private async clickSaveAndConfirm(name: RegExp): Promise<void> {
for (let attempt = 0; SAVE_CONFIRM_RETRIES > attempt; attempt++) {
await this.clickButton(name)

const settled = await this.page.locator(SELECTORS.SAVE_SETTLED_NOTICE).first()
.waitFor({ state: 'visible', timeout: TIMEOUTS.DEFAULT })
.then(() => true)
.catch(() => false)

if (settled) {
return
}

const buttonStillPresent = await this.page.getByRole('button', { name }).first()
.isVisible()
.catch(() => false)

if (!buttonStillPresent) {
return
}
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const SELECTORS = <const>{
LOCATION_SELECT: '.code-snippets-select-location',

SUCCESS_MESSAGE: '.snippet-editor-sidebar .notice.updated',
SAVE_SETTLED_NOTICE: '.snippet-editor-sidebar .notice.updated, .code-snippets-notice.error',

SNIPPETS_TABLE: '.wp-list-table',
SNIPPET_ROW: '.wp-list-table tbody tr',
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/Admin/Menus/Manage_Menu_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public function test_network_bulk_download_requires_network_cap(): void {
)
);

$_POST['snippets'] = wp_json_encode(
$snippets_json = wp_json_encode(
array(
array(
'id' => $snippet->id,
Expand All @@ -246,7 +246,7 @@ public function test_network_bulk_download_requires_network_cap(): void {
$menu = new Manage_Menu();
$method = new ReflectionMethod( $menu, 'get_requested_download_snippets' );
$method->setAccessible( true );
$result = $method->invoke( $menu );
$result = $method->invoke( $menu, $snippets_json );

$this->assertInstanceOf( WP_Error::class, $result );
$this->assertSame( 'code_snippets_forbidden_network_download', $result->get_error_code() );
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/REST_API/REST_API_Cloud_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public function mock_cloud_search_request( $preempt, array $parsed_args, string
*/
private function make_request( array $params ): WP_REST_Response {
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->add_header( 'Access-Control', 'csc-1a2b3c4d5e6f7g8h9i0j' );
$request->add_header( 'Access-Control', $this->get_connection_token() );

foreach ( $params as $key => $value ) {
$request->set_param( $key, $value );
Expand All @@ -160,6 +160,20 @@ private function make_request( array $params ): WP_REST_Response {
return rest_do_request( $request );
}

/**
* Read the active cloud connection's local token.
*
* @return string
*/
private function get_connection_token(): string {
$plugin = \Code_Snippets\code_snippets();

$property = new \ReflectionProperty( $plugin, 'cloud_connection' );
$property->setAccessible( true );

return $property->getValue( $plugin )->get_local_token();
}

/**
* The cloud REST endpoint uses the snippets Screen Options value when per_page is omitted.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ public function test_subsite_admin_can_read_shared_network_snippet() {

$response = $this->dispatch(
'GET',
"/$this->namespace/$this->base_route/$this->shared_snippet_id}",
"/$this->namespace/$this->base_route/$this->shared_snippet_id",
[ 'network' => true ]
);

Expand Down
14 changes: 14 additions & 0 deletions tests/unit/UnitTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,18 @@
*/
class UnitTestCase extends WP_UnitTestCase {

/**
* Set up before each test.
*
* @return void
*/
public function set_up() {
parent::set_up();

if ( is_multisite() ) {
$menu_items = get_site_option( 'menu_items', [] );
$menu_items['snippets'] = 1;
update_site_option( 'menu_items', $menu_items );
}
}
}
Loading