feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run

- Base: wintercms/winter branch 1.2 (full framework)
- Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS
- Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication
- Partials: hero (offline-first), features, modes (offline/nube toggle),
  screenshots, pricing (3 planes), comparison, FAQ, CTA
- Plugin VivesPOS.Site with ContactForm
- Dockerfile: PHP 8.2 Apache, port 80, healthcheck
- Added winter/wn-pages, blog, sitemap, seo plugins
- Active theme set to vivespos
This commit is contained in:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
<?php
namespace System\Tests\Bootstrap;
use System\Classes\UpdateManager;
use System\Classes\VersionManager;
use System\Classes\PluginManager;
use ReflectionClass;
use Winter\Storm\Database\Model as ActiveRecord;
class PluginManagerTestCase extends TestCase
{
public ?PluginManager $manager = null;
protected $output;
/**
* Creates the application.
* @return Symfony\Component\HttpKernel\HttpKernelInterface
*/
public function createApplication()
{
$app = parent::createApplication();
/*
* Store database in memory by default unless specified otherwise
*/
if (!file_exists(base_path('config/testing/database.php'))) {
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
$app['config']->set('database.default', 'testing');
}
return $app;
}
/**
* Perform test case set up.
* @return void
*/
public function setUp() : void
{
/*
* Force reload of Winter singletons
*/
PluginManager::forgetInstance();
UpdateManager::forgetInstance();
// Forces plugin migrations to be run again on every test
VersionManager::forgetInstance();
$this->output = new \Symfony\Component\Console\Output\BufferedOutput();
parent::setUp();
/*
* Ensure system is up to date
*/
$this->runWinterUpCommand();
$manager = PluginManager::instance();
self::callProtectedMethod($manager, 'init');
$this->manager = $manager;
}
/**
* Flush event listeners and collect garbage.
* @return void
*/
public function tearDown() : void
{
$this->flushModelEventListeners();
parent::tearDown();
unset($this->app);
}
/**
* Migrate database using winter:up command.
* @return void
*/
protected function runWinterUpCommand()
{
UpdateManager::instance()
->setNotesOutput($this->output)
->update();
}
/**
* The models in Winter use a static property to store their events, these
* will need to be targeted and reset ready for a new test cycle.
* Pivot models are an exception since they are internally managed.
* @return void
*/
protected function flushModelEventListeners()
{
foreach (get_declared_classes() as $class) {
if ($class === 'Winter\Storm\Database\Pivot' || strtolower($class) === 'october\rain\database\pivot') {
continue;
}
$reflectClass = new ReflectionClass($class);
if (
!$reflectClass->isInstantiable() ||
!$reflectClass->isSubclassOf('Winter\Storm\Database\Model') ||
$reflectClass->isSubclassOf('Winter\Storm\Database\Pivot')
) {
continue;
}
$class::flushEventListeners();
}
ActiveRecord::flushEventListeners();
}
}

View File

@@ -0,0 +1,261 @@
<?php
namespace System\Tests\Bootstrap;
use Mail;
use Config;
use Artisan;
use Exception;
use ReflectionClass;
use Backend\Classes\AuthManager;
use Backend\Tests\Concerns\InteractsWithAuthentication;
use Mockery\MockInterface;
use System\Classes\PluginBase;
use System\Classes\PluginManager;
use System\Classes\UpdateManager;
use Winter\Storm\Database\Model as BaseModel;
/**
* Plugin test case.
*
* The base test case that should be used for plugin tests. It instantiates the given plugin and
* its dependencies, and ensures that the plugin is available for use within the tests.
*
* @package winter/wn-system-module
*/
abstract class PluginTestCase extends TestCase
{
use InteractsWithAuthentication;
/**
* @var array Cache for storing which plugins have been loaded.
*/
protected $pluginTestCaseLoadedPlugins = [];
/**
* Creates the application.
*
* @return \Symfony\Component\HttpKernel\HttpKernelInterface
*/
public function createApplication()
{
$app = require __DIR__ . '/../../../../bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
$app['cache']->setDefaultDriver('array');
$app->setLocale('en');
$app->singleton('backend.auth', function ($app) {
$app['auth.loaded'] = true;
return AuthManager::instance();
});
// Store database in memory by default unless otherwise specified
if (!file_exists(base_path('config/testing/database.php'))) {
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
$app['config']->set('database.default', 'testing');
}
// Set random encryption key
$app['config']->set('app.key', bin2hex(random_bytes(16)));
// Modify the plugin path away from the test context
$app->setPluginsPath(realpath(base_path() . Config::get('cms.pluginsPath')));
return $app;
}
/**
* Perform test case set up.
*/
public function setUp(): void
{
// Reload the plugin and update manager singletons
PluginManager::forgetInstance();
UpdateManager::forgetInstance();
parent::setUp();
// Reset loaded routes
$this->app['router']->setRoutes(new \Illuminate\Routing\RouteCollection);
// Run all migrations
Artisan::call('winter:up');
// Reset loaded plugins for this test
$this->pluginTestCaseLoadedPlugins = [];
// Detect the plugin being tested and load it automatically
$pluginCode = $this->guessPluginCode();
if (!is_null($pluginCode)) {
$this->instantiatePlugin($pluginCode, false);
}
// Reload module routes
foreach (Config::get('cms.loadModules', []) as $module) {
include base_path("modules/" . strtolower($module) . "/routes.php");
}
// Disable mailing
Mail::pretend();
}
/**
* Flush event listeners and tear down.
*/
public function tearDown(): void
{
$this->flushModelEventListeners();
parent::tearDown();
unset($this->app);
}
/**
* Refreshes a plugin for testing.
*
* Since the test environment has loaded all the test plugins natively, this method will ensure
* the desired plugin is loaded in the system before proceeding to migrate it.
*
* @deprecated v1.2.1 Use `instantiatePlugin()` instead.
* @return void
*/
protected function runPluginRefreshCommand($code, $throwException = true)
{
$this->instantiatePlugin((string) $code, (bool) $throwException);
}
/**
* Instantiates a plugin for testing.
*
* @param string $code Plugin code.
* @param boolean $throw Throw an exception if the plugin cannot be found.
*/
protected function instantiatePlugin(string $code, bool $throw = true): void
{
// Check plugin code is valid
if (!preg_match('/^[\w+]*\.[\w+]*$/', $code)) {
if (!$throw) {
return;
}
throw new Exception(sprintf('Invalid plugin code: "%s"', $code));
}
$manager = PluginManager::instance();
$plugin = $manager->findByIdentifier($code);
$firstLoad = !$plugin;
// First time seeing this plugin, load it up
if ($firstLoad) {
$namespace = '\\'.str_replace('.', '\\', $code);
$path = array_get(
array_change_key_case($manager->getPluginNamespaces(), CASE_LOWER),
strtolower($namespace)
);
if (!$path) {
if (!$throw) {
return;
}
throw new Exception(sprintf('Unable to find plugin with code: "%s"', $code));
}
$plugin = $manager->loadPlugin($namespace, $path);
$manager->registerPlugin($plugin);
}
$this->pluginTestCaseLoadedPlugins[$code] = $plugin;
// Load any dependencies
if (!empty($plugin->require)) {
foreach ((array) $plugin->require as $dependency) {
if (isset($this->pluginTestCaseLoadedPlugins[$dependency])) {
continue;
}
$this->instantiatePlugin($dependency);
}
}
// Refresh the plugin's tables
Artisan::call('plugin:refresh', ['plugin' => $code, '--force' => true]);
// Boot the plugin if this is the first load
if ($firstLoad) {
$manager->bootPlugin($plugin);
}
}
/**
* Returns a plugin object from its code, useful for registering events, etc.
*/
protected function getPluginObject($code = null): ?PluginBase
{
return $this->pluginTestCaseLoadedPlugins[$code]
?? $this->pluginTestCaseLoadedPlugins[$this->guessPluginCode()]
?? null;
}
/**
* Flush model event listeners.
*
* The models in Winter use a static property to store their events. These will need to be
* targeted and reset, ready for a new test cycle.
*
* Pivot models are an exception since they are internally managed.
*/
protected function flushModelEventListeners(): void
{
foreach (get_declared_classes() as $class) {
if ($class === 'Winter\Storm\Database\Pivot' || strtolower($class) === 'october\rain\database\pivot') {
continue;
}
$reflectClass = new ReflectionClass($class);
if (
!$reflectClass->isInstantiable() ||
!$reflectClass->isSubclassOf('Winter\Storm\Database\Model') ||
$reflectClass->isSubclassOf('Winter\Storm\Database\Pivot') ||
in_array(MockInterface::class, $reflectClass->getInterfaceNames())
) {
continue;
}
$class::flushEventListeners();
}
BaseModel::flushEventListeners();
}
/**
* Guesses the plugin code being tested.
*/
protected function guessPluginCode(): ?string
{
$reflect = new ReflectionClass($this);
$fqClass = $reflect->getName();
$namespace = $reflect->getNamespaceName();
if (empty($namespace)) {
// Try to determine from the path instead
$path = $reflect->getFilename();
$basePath = $this->app->pluginsPath();
if (!strpos($path, $basePath) === 0) {
return null;
}
$pluginCode = ltrim(str_replace('\\', '/', substr($path, strlen($basePath))), '/');
$pluginCode = implode('.', array_slice(explode('/', $pluginCode), 0, 2));
} else {
// Determine code from namespace
$manager = PluginManager::instance();
$pluginCode = $manager->getIdentifier($fqClass);
}
return $pluginCode;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace System\Tests\Bootstrap;
use ReflectionClass;
use PHPUnit\Framework\Assert;
class TestCase extends \Illuminate\Foundation\Testing\TestCase
{
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__ . '/../../../../bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
$app['cache']->setDefaultDriver('array');
$app->setLocale('en');
// Set random encryption key
$app['config']->set('app.key', bin2hex(random_bytes(16)));
return $app;
}
//
// Helpers
//
protected static function callProtectedMethod($object, $name, $params = [])
{
$className = get_class($object);
$class = new ReflectionClass($className);
$method = $class->getMethod($name);
$method->setAccessible(true);
return $method->invokeArgs($object, $params);
}
public static function getProtectedProperty($object, $name)
{
$className = get_class($object);
$class = new ReflectionClass($className);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property->getValue($object);
}
public static function setProtectedProperty($object, $name, $value)
{
$className = get_class($object);
$class = new ReflectionClass($className);
$property = $class->getProperty($name);
$property->setAccessible(true);
return $property->setValue($object, $value);
}
/**
* Stub for `assertFileNotExists` to allow compatibility with both PHPUnit 8 and 9.
*
* @param string $filename
* @param string $message
* @return void
*/
public static function assertFileNotExists(string $filename, string $message = ''): void
{
if (method_exists(Assert::class, 'assertFileDoesNotExist')) {
Assert::assertFileDoesNotExist($filename, $message);
return;
}
Assert::assertFileNotExists($filename, $message);
}
/**
* Stub for `assertRegExp` to allow compatibility with both PHPUnit 8 and 9.
*
* @param string $filename
* @param string $message
* @return void
*/
public static function assertRegExp(string $pattern, string $string, string $message = ''): void
{
if (method_exists(Assert::class, 'assertMatchesRegularExpression')) {
Assert::assertMatchesRegularExpression($pattern, $string, $message);
return;
}
Assert::assertRegExp($pattern, $string, $message);
}
}

View File

@@ -0,0 +1,59 @@
<?php
$baseDir = realpath(__DIR__ . '/../../../..');
/*
* Winter autoloader
*/
require $baseDir . '/bootstrap/autoload.php';
/*
* Fallback autoloader
*/
$loader = new Winter\Storm\Support\ClassLoader(
new Winter\Storm\Filesystem\Filesystem,
$baseDir,
$baseDir . '/storage/framework/classes.php'
);
$loader->register();
/*
* Manually register all module classes for autoloading
*/
foreach (glob($baseDir . '/modules/*', GLOB_ONLYDIR) as $modulePath) {
$loader->autoloadPackage(basename($modulePath), $modulePath);
}
/*
* Manually register System aliases
*/
foreach (require(__DIR__ . '/../../aliases.php') as $alias => $class) {
if (!class_exists($alias)) {
class_alias($class, $alias);
}
}
/*
* Manually register all plugin classes for autoloading
*/
$dirPath = $baseDir . '/plugins';
if (is_dir($dirPath)) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dirPath, FilesystemIterator::FOLLOW_SYMLINKS)
);
$it->setMaxDepth(2);
$it->rewind();
while ($it->valid()) {
if (($it->getDepth() > 1) && $it->isFile() && (strtolower($it->getFilename()) === "plugin.php")) {
$filePath = dirname($it->getPathname());
$pluginName = basename($filePath);
$vendorName = basename(dirname($filePath));
$loader->autoloadPackage($vendorName . '\\' . $pluginName, $filePath);
}
$it->next();
}
}