Skip to content

Commit af23e74

Browse files
Add test coverage, PHPCS lint, and CI lint/coverage jobs (#4)
* Bump version to 1.3.0: admin UI polish and CI/CD hardening - Add settings page header card (version badge, status pill, provider chip, GitHub link) - Rewrite admin CSS to WP-native system fonts; remove Montserrat/Roboto Google Fonts CDN - Source/provider columns in entries list now render colored badge pills - Export CSV moved to WP-native page-title-action area - Detail view source/provider rows render colored badge pills - CI: release workflow verifies plugin version matches git tag before building - CI: release workflow caches Composer deps across PHP matrix jobs - CI: build-zip workflow triggers on push to main in addition to PRs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add test coverage, PHPCS lint, and CI lint/coverage jobs - Add 25 unit tests for PackRelay_Entries_List_Table (badge columns, structure) - Add 3 unit tests for PackRelay_Entries_Page (CSV cell casting, get_view_url) - Add .phpcs.xml with generic PHP coding standards config - Add squizlabs/php_codesniffer to require-dev + lint script to composer.json - Add lint (PHPCS) and coverage (pcov) jobs to test.yml CI matrix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 02b47cf commit af23e74

5 files changed

Lines changed: 288 additions & 2 deletions

File tree

.github/workflows/test.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,53 @@ jobs:
4848

4949
- name: Run tests
5050
run: vendor/bin/phpunit
51+
52+
lint:
53+
name: Lint (PHPCS)
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- name: Setup PHP
59+
uses: shivammathur/setup-php@v2
60+
with:
61+
php-version: '8.3'
62+
coverage: none
63+
64+
- name: Cache Composer dependencies
65+
uses: actions/cache@v4
66+
with:
67+
path: vendor
68+
key: composer-8.3-${{ hashFiles('**/composer.lock', '**/composer.json') }}
69+
restore-keys: composer-8.3-
70+
71+
- name: Install dependencies
72+
run: composer install --prefer-dist --no-progress
73+
74+
- name: Run PHPCS
75+
run: vendor/bin/phpcs
76+
77+
coverage:
78+
name: Coverage (PHP 8.3)
79+
runs-on: ubuntu-latest
80+
steps:
81+
- uses: actions/checkout@v4
82+
83+
- name: Setup PHP
84+
uses: shivammathur/setup-php@v2
85+
with:
86+
php-version: '8.3'
87+
coverage: pcov
88+
89+
- name: Cache Composer dependencies
90+
uses: actions/cache@v4
91+
with:
92+
path: vendor
93+
key: composer-8.3-${{ hashFiles('**/composer.lock', '**/composer.json') }}
94+
restore-keys: composer-8.3-
95+
96+
- name: Install dependencies
97+
run: composer install --prefer-dist --no-progress
98+
99+
- name: Run tests with coverage
100+
run: vendor/bin/phpunit --coverage-text

.phpcs.xml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="PackRelay">
3+
<description>PackRelay PHP coding standards.</description>
4+
5+
<file>includes</file>
6+
<file>admin</file>
7+
<file>packrelay.php</file>
8+
<file>uninstall.php</file>
9+
10+
<arg name="basepath" value="."/>
11+
<arg name="colors"/>
12+
<arg name="extensions" value="php"/>
13+
<arg value="sp"/>
14+
15+
<!-- Target PHP 8.1+. -->
16+
<config name="php_version" value="80100"/>
17+
18+
<!-- No short open tags. -->
19+
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
20+
21+
<!-- No silenced errors (@). -->
22+
<rule ref="Generic.PHP.NoSilencedErrors"/>
23+
24+
<!-- No backtick operator. -->
25+
<rule ref="Generic.PHP.BacktickOperator"/>
26+
27+
<!-- No deprecated PHP functions. -->
28+
<rule ref="Generic.PHP.DeprecatedFunctions"/>
29+
30+
<!-- One statement per line. -->
31+
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
32+
33+
<!-- No byte-order mark. -->
34+
<rule ref="Generic.Files.ByteOrderMark"/>
35+
36+
<!-- Defined constants must be uppercase. -->
37+
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
38+
39+
<!-- No PHP 4 constructor names. -->
40+
<rule ref="Generic.NamingConventions.ConstructorName"/>
41+
</ruleset>

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"require-dev": {
1818
"phpunit/phpunit": "^10.0",
1919
"brain/monkey": "^2.6",
20-
"mockery/mockery": "^1.6"
20+
"mockery/mockery": "^1.6",
21+
"squizlabs/php_codesniffer": "^3.10"
2122
},
2223
"autoload-dev": {
2324
"psr-4": {
@@ -26,7 +27,8 @@
2627
},
2728
"scripts": {
2829
"test": "phpunit",
29-
"test:coverage": "phpunit --coverage-text"
30+
"test:coverage": "phpunit --coverage-text",
31+
"lint": "phpcs"
3032
},
3133
"config": {
3234
"sort-packages": true,

tests/EntriesListTableTest.php

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
/**
3+
* Tests for PackRelay_Entries_List_Table.
4+
*
5+
* @package PackRelay
6+
* @copyright 2026 MrDemonWolf, Inc.
7+
*/
8+
9+
namespace PackRelay\Tests;
10+
11+
class EntriesListTableTest extends TestCase {
12+
13+
private \PackRelay_Entries_List_Table $table;
14+
15+
protected function setUp(): void {
16+
parent::setUp();
17+
18+
global $wpdb;
19+
$wpdb = \Mockery::mock();
20+
$wpdb->prefix = 'wp_';
21+
22+
$this->table = new \PackRelay_Entries_List_Table();
23+
}
24+
25+
// === column_source ===
26+
27+
public function test_column_source_mobile_badge_for_divi(): void {
28+
$result = $this->table->column_source( array( 'provider' => 'divi' ) );
29+
$this->assertStringContainsString( 'packrelay-source-badge mobile', $result );
30+
$this->assertStringContainsString( 'Mobile App', $result );
31+
}
32+
33+
public function test_column_source_mobile_badge_for_wpforms(): void {
34+
$result = $this->table->column_source( array( 'provider' => 'wpforms' ) );
35+
$this->assertStringContainsString( 'packrelay-source-badge mobile', $result );
36+
}
37+
38+
public function test_column_source_mobile_badge_for_gravityforms(): void {
39+
$result = $this->table->column_source( array( 'provider' => 'gravityforms' ) );
40+
$this->assertStringContainsString( 'packrelay-source-badge mobile', $result );
41+
}
42+
43+
public function test_column_source_frontend_badge_for_divi_frontend(): void {
44+
$result = $this->table->column_source( array( 'provider' => 'divi_frontend' ) );
45+
$this->assertStringContainsString( 'packrelay-source-badge frontend', $result );
46+
$this->assertStringContainsString( 'Divi Frontend', $result );
47+
}
48+
49+
public function test_column_source_returns_span_tag(): void {
50+
$result = $this->table->column_source( array( 'provider' => 'divi' ) );
51+
$this->assertStringStartsWith( '<span', $result );
52+
$this->assertStringEndsWith( '</span>', $result );
53+
}
54+
55+
// === column_provider ===
56+
57+
public function test_column_provider_badge_for_divi(): void {
58+
$result = $this->table->column_provider( array( 'provider' => 'divi' ) );
59+
$this->assertStringContainsString( 'packrelay-provider-badge', $result );
60+
$this->assertStringContainsString( 'Divi', $result );
61+
}
62+
63+
public function test_column_provider_badge_for_divi_frontend(): void {
64+
$result = $this->table->column_provider( array( 'provider' => 'divi_frontend' ) );
65+
$this->assertStringContainsString( 'packrelay-provider-badge', $result );
66+
$this->assertStringContainsString( 'Divi', $result );
67+
}
68+
69+
public function test_column_provider_badge_for_wpforms(): void {
70+
$result = $this->table->column_provider( array( 'provider' => 'wpforms' ) );
71+
$this->assertStringContainsString( 'packrelay-provider-badge', $result );
72+
$this->assertStringContainsString( 'WPForms', $result );
73+
}
74+
75+
public function test_column_provider_badge_for_gravityforms(): void {
76+
$result = $this->table->column_provider( array( 'provider' => 'gravityforms' ) );
77+
$this->assertStringContainsString( 'packrelay-provider-badge', $result );
78+
$this->assertStringContainsString( 'Gravity Forms', $result );
79+
}
80+
81+
public function test_column_provider_falls_back_to_raw_value_for_unknown_provider(): void {
82+
$result = $this->table->column_provider( array( 'provider' => 'custom_builder' ) );
83+
$this->assertStringContainsString( 'packrelay-provider-badge', $result );
84+
$this->assertStringContainsString( 'custom_builder', $result );
85+
}
86+
87+
// === other columns ===
88+
89+
public function test_column_form_id_returns_escaped_value(): void {
90+
$result = $this->table->column_form_id( array( 'form_id' => '42:0' ) );
91+
$this->assertSame( '42:0', $result );
92+
}
93+
94+
public function test_column_ip_address_returns_value(): void {
95+
$result = $this->table->column_ip_address( array( 'ip_address' => '127.0.0.1' ) );
96+
$this->assertSame( '127.0.0.1', $result );
97+
}
98+
99+
public function test_column_date_created_returns_date_string(): void {
100+
$result = $this->table->column_date_created( array( 'date_created' => '2026-06-11 12:00:00' ) );
101+
$this->assertSame( '2026-06-11 12:00:00', $result );
102+
}
103+
104+
public function test_column_fields_returns_mdash_for_invalid_json(): void {
105+
$result = $this->table->column_fields( array( 'fields' => 'not-json' ) );
106+
$this->assertSame( '&mdash;', $result );
107+
}
108+
109+
public function test_column_fields_returns_mdash_for_non_array_json(): void {
110+
$result = $this->table->column_fields( array( 'fields' => '"just a string"' ) );
111+
$this->assertSame( '&mdash;', $result );
112+
}
113+
114+
public function test_column_fields_renders_key_value_preview(): void {
115+
$fields = json_encode( array( 'name' => 'John Doe', 'email' => 'john@example.com' ) );
116+
$result = $this->table->column_fields( array( 'fields' => $fields ) );
117+
$this->assertStringContainsString( 'name', $result );
118+
$this->assertStringContainsString( 'John Doe', $result );
119+
$this->assertStringContainsString( 'email', $result );
120+
}
121+
122+
public function test_column_fields_limits_preview_to_three_fields(): void {
123+
$fields = json_encode( array( 'a' => '1', 'b' => '2', 'c' => '3', 'd' => '4' ) );
124+
$result = $this->table->column_fields( array( 'fields' => $fields ) );
125+
// At most 3 key: value pairs, each separated by <br>.
126+
$this->assertLessThanOrEqual( 2, substr_count( $result, '<br>' ) );
127+
}
128+
129+
public function test_column_cb_contains_entry_id(): void {
130+
$result = $this->table->column_cb( array( 'id' => 99 ) );
131+
$this->assertStringContainsString( 'value="99"', $result );
132+
$this->assertStringContainsString( 'entry_ids[]', $result );
133+
}
134+
135+
// === structure ===
136+
137+
public function test_get_columns_contains_required_keys(): void {
138+
$columns = $this->table->get_columns();
139+
foreach ( array( 'cb', 'id', 'source', 'provider', 'form_id', 'fields', 'ip_address', 'date_created' ) as $key ) {
140+
$this->assertArrayHasKey( $key, $columns, "Missing column: $key" );
141+
}
142+
}
143+
144+
public function test_get_sortable_columns_includes_id_and_date(): void {
145+
$sortable = $this->table->get_sortable_columns();
146+
$this->assertArrayHasKey( 'id', $sortable );
147+
$this->assertArrayHasKey( 'date_created', $sortable );
148+
}
149+
150+
public function test_get_bulk_actions_includes_delete(): void {
151+
$actions = $this->table->get_bulk_actions();
152+
$this->assertArrayHasKey( 'delete', $actions );
153+
$this->assertSame( 'Delete', $actions['delete'] );
154+
}
155+
}

tests/EntriesPageTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,42 @@ public function test_sanitize_csv_cell_leaves_safe_values_unchanged(): void {
6060
$this->assertSame( '', $this->page->sanitize_csv_cell( '' ) );
6161
$this->assertSame( '42', $this->page->sanitize_csv_cell( 42 ) );
6262
}
63+
64+
public function test_sanitize_csv_cell_casts_numeric_types_to_string(): void {
65+
$this->assertSame( '0', $this->page->sanitize_csv_cell( 0 ) );
66+
$this->assertSame( '3.14', $this->page->sanitize_csv_cell( 3.14 ) );
67+
}
68+
69+
public function test_get_view_url_includes_entry_id_and_nonce(): void {
70+
Functions\when( 'admin_url' )->justReturn( 'http://example.com/wp-admin/admin.php' );
71+
Functions\when( 'add_query_arg' )->alias(
72+
function ( $args, $url ) {
73+
return $url . '?' . http_build_query( $args );
74+
}
75+
);
76+
Functions\when( 'wp_nonce_url' )->alias(
77+
function ( $url, $action ) {
78+
return $url . '&_wpnonce=testhash';
79+
}
80+
);
81+
82+
$url = \PackRelay_Entries_Page::get_view_url( 7 );
83+
84+
$this->assertStringContainsString( 'entry_id=7', $url );
85+
$this->assertStringContainsString( 'action=view', $url );
86+
$this->assertStringContainsString( '_wpnonce=', $url );
87+
}
88+
89+
public function test_get_view_url_casts_entry_id_to_positive_int(): void {
90+
Functions\when( 'admin_url' )->justReturn( 'http://example.com/wp-admin/admin.php' );
91+
Functions\when( 'add_query_arg' )->alias(
92+
function ( $args, $url ) {
93+
return $url . '?' . http_build_query( $args );
94+
}
95+
);
96+
Functions\when( 'wp_nonce_url' )->returnArg();
97+
98+
$url = \PackRelay_Entries_Page::get_view_url( '5abc' );
99+
$this->assertStringContainsString( 'entry_id=5', $url );
100+
}
63101
}

0 commit comments

Comments
 (0)