View a policy version's content, and make policy text readable
CI / Tests (PHP 8.1) (pull_request) Successful in 50s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m12s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m50s
CI / PHPStan (pull_request) Successful in 3m4s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 50s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m12s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m50s
CI / PHPStan (pull_request) Successful in 3m4s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
The Policies admin page listed versions but never showed what any of them said, so revising a policy meant retyping it blind into an empty draft box. Each version row now has a View action that renders that version's text on the page, editable in place. A draft is saved back to itself; editing a published or archived version branches a new draft and leaves the original alone, because acceptances are recorded against policy_version_id and text a student agreed to must stay exactly as they saw it. That viewer also exposed why a studio reported the acceptance box as unreadable — one squashed line, overlapping words, a horizontal scrollbar. Bodies are typed into a bare textarea, so most carry no markup, and the raw text was emitted with its blank lines intact but nothing to turn them into paragraphs. PolicyVersion::bodyHtml() now renders every body the way WordPress renders post content (kses, then wpautop) and feeds all three consumers: the booking/enrolment JSON, the signup form, and the new viewer. Bodies written with markup are unaffected. The other half was that .us-policy-body had no CSS whatsoever and inherited whatever the theme did with an unstyled block in a form. It is now a bounded reading box that scrolls vertically and breaks long tokens, so a pasted URL cannot force the page sideways and a long policy cannot push the accept checkbox out of view. RegistrationPage was also never enqueueing the plugin stylesheet, which is why the signup gate looked worst of all. Closes #126 Closes #127 Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Unsupervised\Schedular\Tests\Unit\Policy;
|
||||
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
use Unsupervised\Schedular\Policy\Policy;
|
||||
use Unsupervised\Schedular\Policy\PolicyController;
|
||||
use Unsupervised\Schedular\Policy\PolicyRepository;
|
||||
use Unsupervised\Schedular\Policy\PolicyService;
|
||||
use Unsupervised\Schedular\Policy\PolicyVersion;
|
||||
use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
||||
use Unsupervised\Schedular\Tests\Unit\TestCase;
|
||||
|
||||
class PolicyControllerTest extends TestCase
|
||||
{
|
||||
private PolicyRepository&Mockery\MockInterface $policies;
|
||||
private PolicyVersionRepository&Mockery\MockInterface $versions;
|
||||
private PolicyService&Mockery\MockInterface $service;
|
||||
private PolicyController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->policies = Mockery::mock(PolicyRepository::class);
|
||||
$this->versions = Mockery::mock(PolicyVersionRepository::class);
|
||||
$this->service = Mockery::mock(PolicyService::class);
|
||||
$this->controller = new PolicyController($this->policies, $this->versions, $this->service);
|
||||
|
||||
$_POST = [];
|
||||
$_GET = [];
|
||||
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
Functions\when('check_admin_referer')->justReturn(true);
|
||||
Functions\when('wp_unslash')->returnArg();
|
||||
Functions\when('sanitize_text_field')->returnArg();
|
||||
Functions\when('sanitize_title')->returnArg();
|
||||
Functions\when('wp_kses_post')->returnArg();
|
||||
$this->stubAutop();
|
||||
Functions\when('sanitize_key')->alias(
|
||||
static fn ($key) => strtolower((string) preg_replace('/[^a-zA-Z0-9_\-]/', '', (string) $key))
|
||||
);
|
||||
Functions\when('absint')->alias(static fn ($value) => abs((int) $value));
|
||||
Functions\when('selected')->justReturn('');
|
||||
Functions\when('wp_nonce_field')->justReturn('');
|
||||
Functions\when('admin_url')->justReturn('admin.php');
|
||||
Functions\when('add_query_arg')->alias(
|
||||
static fn (array $args, string $url) => $url . '?' . http_build_query($args)
|
||||
);
|
||||
Functions\when('submit_button')->alias(static function (string $text = ''): void {
|
||||
echo $text; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- test stub
|
||||
});
|
||||
}
|
||||
|
||||
public function testViewingAVersionRendersItsBodyAndAnEditForm(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', currentVersionId: 7, id: 4);
|
||||
$version = new PolicyVersion(
|
||||
policyId: 4,
|
||||
versionNumber: 2,
|
||||
body: '<p>Cancel 24 hours ahead.</p>',
|
||||
status: PolicyVersion::STATUS_PUBLISHED,
|
||||
publishedAt: '2026-07-01 09:00:00',
|
||||
id: 7,
|
||||
);
|
||||
|
||||
$_GET = [
|
||||
'policy_id' => '4',
|
||||
'version_id' => '7',
|
||||
];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$version]);
|
||||
$this->versions->shouldReceive('findById')->with(7)->andReturn($version);
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
self::assertStringContainsString('Cancel 24 hours ahead.', $html);
|
||||
self::assertStringContainsString('value="edit_version"', $html);
|
||||
self::assertStringContainsString('Save as New Draft', $html);
|
||||
}
|
||||
|
||||
public function testVersionListLinksToTheViewer(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', currentVersionId: 7, id: 4);
|
||||
$version = new PolicyVersion(policyId: 4, versionNumber: 2, body: 'text', id: 7);
|
||||
|
||||
$_GET = ['policy_id' => '4'];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$version]);
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
self::assertStringContainsString('page=us-policies&policy_id=4&version_id=7', $html);
|
||||
}
|
||||
|
||||
public function testAVersionBelongingToAnotherPolicyIsNotShown(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', id: 4);
|
||||
$other = new PolicyVersion(policyId: 9, versionNumber: 1, body: 'Someone else\'s policy', id: 7);
|
||||
|
||||
$_GET = [
|
||||
'policy_id' => '4',
|
||||
'version_id' => '7',
|
||||
];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([]);
|
||||
$this->versions->shouldReceive('findById')->with(7)->andReturn($other);
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
self::assertStringNotContainsString('Someone else\'s policy', $html);
|
||||
self::assertStringNotContainsString('value="edit_version"', $html);
|
||||
}
|
||||
|
||||
public function testEditingAPublishedVersionCreatesANewDraftAndLeavesTheSourceUntouched(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', currentVersionId: 7, id: 4);
|
||||
$published = new PolicyVersion(
|
||||
policyId: 4,
|
||||
versionNumber: 2,
|
||||
body: '<p>Old text.</p>',
|
||||
status: PolicyVersion::STATUS_PUBLISHED,
|
||||
id: 7,
|
||||
);
|
||||
$draft = new PolicyVersion(policyId: 4, versionNumber: 3, body: '<p>New text.</p>', id: 11);
|
||||
|
||||
$_GET = ['policy_id' => '4'];
|
||||
$_POST = [
|
||||
'usc_action' => 'edit_version',
|
||||
'policy_id' => '4',
|
||||
'version_id' => '7',
|
||||
'body' => '<p>New text.</p>',
|
||||
];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$draft, $published]);
|
||||
$this->versions->shouldReceive('findById')->with(7)->andReturn($published);
|
||||
$this->versions->shouldReceive('findById')->with(11)->andReturn($draft);
|
||||
|
||||
// A new draft is written; the edited version itself is never updated.
|
||||
$this->service->shouldReceive('addDraftVersion')->once()->with(4, '<p>New text.</p>')->andReturn(11);
|
||||
$this->versions->shouldNotReceive('updateBody');
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
// The viewer follows through to the draft that was just created.
|
||||
self::assertStringContainsString('New text.', $html);
|
||||
self::assertStringContainsString('saved as a new draft version', $html);
|
||||
}
|
||||
|
||||
public function testEditingADraftVersionUpdatesItInPlace(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', id: 4);
|
||||
$draft = new PolicyVersion(
|
||||
policyId: 4,
|
||||
versionNumber: 3,
|
||||
body: '<p>Old draft text.</p>',
|
||||
status: PolicyVersion::STATUS_DRAFT,
|
||||
id: 11,
|
||||
);
|
||||
|
||||
$_GET = ['policy_id' => '4'];
|
||||
$_POST = [
|
||||
'usc_action' => 'edit_version',
|
||||
'policy_id' => '4',
|
||||
'version_id' => '11',
|
||||
'body' => '<p>Revised draft text.</p>',
|
||||
];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$draft]);
|
||||
$this->versions->shouldReceive('findById')->with(11)->andReturn($draft);
|
||||
|
||||
// An unpublished draft is rewritten rather than branched.
|
||||
$this->versions->shouldReceive('updateBody')->once()->with(11, '<p>Revised draft text.</p>')->andReturn(true);
|
||||
$this->service->shouldNotReceive('addDraftVersion');
|
||||
|
||||
$html = $this->render();
|
||||
|
||||
self::assertStringContainsString('Draft version 3 was updated', $html);
|
||||
}
|
||||
|
||||
public function testEditingAnArchivedVersionCreatesANewDraft(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', currentVersionId: 9, id: 4);
|
||||
$archived = new PolicyVersion(
|
||||
policyId: 4,
|
||||
versionNumber: 1,
|
||||
body: '<p>Superseded text.</p>',
|
||||
status: PolicyVersion::STATUS_ARCHIVED,
|
||||
id: 7,
|
||||
);
|
||||
$draft = new PolicyVersion(policyId: 4, versionNumber: 4, body: '<p>Reinstated text.</p>', id: 12);
|
||||
|
||||
$_GET = ['policy_id' => '4'];
|
||||
$_POST = [
|
||||
'usc_action' => 'edit_version',
|
||||
'policy_id' => '4',
|
||||
'version_id' => '7',
|
||||
'body' => '<p>Reinstated text.</p>',
|
||||
];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([$draft, $archived]);
|
||||
$this->versions->shouldReceive('findById')->with(7)->andReturn($archived);
|
||||
$this->versions->shouldReceive('findById')->with(12)->andReturn($draft);
|
||||
|
||||
$this->service->shouldReceive('addDraftVersion')->once()->with(4, '<p>Reinstated text.</p>')->andReturn(12);
|
||||
$this->versions->shouldNotReceive('updateBody');
|
||||
|
||||
$this->render();
|
||||
}
|
||||
|
||||
public function testEditingAVersionOfAnotherPolicyIsRejected(): void
|
||||
{
|
||||
$policy = new Policy('Cancellation', 'cancellation', id: 4);
|
||||
$other = new PolicyVersion(policyId: 9, versionNumber: 1, body: 'Other', id: 7);
|
||||
|
||||
$_GET = ['policy_id' => '4'];
|
||||
$_POST = [
|
||||
'usc_action' => 'edit_version',
|
||||
'policy_id' => '4',
|
||||
'version_id' => '7',
|
||||
'body' => 'Injected text',
|
||||
];
|
||||
|
||||
$this->policies->shouldReceive('findAll')->andReturn([$policy]);
|
||||
$this->policies->shouldReceive('findById')->with(4)->andReturn($policy);
|
||||
$this->versions->shouldReceive('findByPolicy')->with(4)->andReturn([]);
|
||||
$this->versions->shouldReceive('findById')->with(7)->andReturn($other);
|
||||
|
||||
$this->service->shouldNotReceive('addDraftVersion');
|
||||
$this->versions->shouldNotReceive('updateBody');
|
||||
|
||||
$this->render();
|
||||
}
|
||||
|
||||
private function render(): string
|
||||
{
|
||||
ob_start();
|
||||
$this->controller->renderPage();
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user