Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .ddev/commands/web/phpunit
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

set -euo pipefail

## Description: Run PHPUnit tests for the MageForge module
## Usage: phpunit [options]
## Example: ddev phpunit
## Example: ddev phpunit --testdox
## Example: ddev phpunit tests/Unit/Service/Hyva/

cd /var/www/html

if [[ ! -x vendor/bin/phpunit ]]; then
echo "phpunit not found. Installing module dev dependencies..."
composer install --no-interaction
fi

vendor/bin/phpunit "$@"
37 changes: 37 additions & 0 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: PHPUnit

on: [pull_request]

permissions:
contents: read

env:
PHP_VERSION: "8.4"

jobs:
phpunit:
name: Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
with:
php-version: ${{ env.PHP_VERSION }}
tools: composer:v2

- name: Cache Composer packages
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.composer/cache/files
key: ${{ runner.os }}-composer-phpunit-${{ hashFiles('composer.json') }}
restore-keys: ${{ runner.os }}-composer-phpunit

- name: Install dev dependencies
run: composer install --no-interaction --no-progress

- name: Run PHPUnit
run: vendor/bin/phpunit
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
/vendor/
/composer.lock

# PHPUnit
/.phpunit.cache/

.vscode
/magento/
/magento-temp/
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
},
"require-dev": {
"carthage-software/mago": "^1.30",
"magento/magento-coding-standard": "^40"
"magento/magento-coding-standard": "^40",
"phpunit/phpunit": "^11.0"
},
"autoload-dev": {
"psr-4": {
"OpenForgeProject\\MageForge\\Test\\": "tests/"
}
},
"repositories": [
{
Expand Down
17 changes: 17 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
16 changes: 4 additions & 12 deletions src/Console/Command/Hyva/CompatibilityCheckCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ private function runInteractiveMode(InputInterface $input, OutputInterface $outp
$this->io->newLine();

// Run scan with selected options
return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, $incompatibleOnly, $output);
return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, $incompatibleOnly);
} catch (\Throwable $e) {
$this->io->error('Interactive mode failed: ' . $e->getMessage());
$this->io->info('Falling back to default scan (third-party modules only)...');
Expand Down Expand Up @@ -204,7 +204,7 @@ private function runDirectMode(InputInterface $input, OutputInterface $output):

$this->io->title('Hyvä Theme Compatibility Check');

return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, false, $output);
return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, false);
}

/**
Expand All @@ -215,7 +215,6 @@ private function runDirectMode(InputInterface $input, OutputInterface $output):
* @param bool $includeVendor
* @param bool $detailed
* @param bool $incompatibleOnly
* @param OutputInterface $output
* @return int
*/
private function runScan(
Expand All @@ -224,7 +223,6 @@ private function runScan(
bool $includeVendor,
bool $detailed,
bool $incompatibleOnly,
OutputInterface $output,
): int {
// Determine filter logic:
// - thirdPartyOnly: Only scan non-Magento_* modules (default behavior)
Expand All @@ -234,13 +232,7 @@ private function runScan(
$excludeVendor = false;
Comment on lines 228 to 232

// Run the compatibility check
$results = $this->compatibilityChecker->check(
$this->io,
$output,
$showAll,
$scanThirdPartyOnly,
$excludeVendor,
);
$results = $this->compatibilityChecker->check($this->io, $showAll, $scanThirdPartyOnly, $excludeVendor);

// Determine display mode:
// showAll = show all modules including compatible ones
Expand Down Expand Up @@ -310,7 +302,7 @@ private function displayDetailedIssues(array $results): void

$this->io->text(sprintf('<fg=cyan>%s</>', $moduleName));

$detailedIssues = $this->compatibilityChecker->getDetailedIssues($moduleName, $moduleData);
$detailedIssues = $this->compatibilityChecker->getDetailedIssues($moduleData);

foreach ($detailedIssues as $fileData) {
$this->io->text(sprintf(' <fg=yellow>%s</>', $fileData['file']));
Expand Down
23 changes: 11 additions & 12 deletions src/Service/Hyva/CompatibilityChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Magento\Framework\Component\ComponentRegistrar;
use Magento\Framework\Component\ComponentRegistrarInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
Expand Down Expand Up @@ -54,7 +53,6 @@ public function __construct(
* Check all modules for Hyvä compatibility
*
* @param SymfonyStyle $io Symfony Style IO for output
* @param OutputInterface $output Console output interface
* @param bool $showAll Whether to show all modules (including compatible ones)
* @param bool $thirdPartyOnly Whether to scan only third-party modules (excludes Magento_* modules)
* @param bool $excludeVendor Whether to exclude modules from the vendor/ directory
Expand All @@ -64,7 +62,6 @@ public function __construct(
*/
public function check(
SymfonyStyle $io,
OutputInterface $output,
bool $showAll = false,
bool $thirdPartyOnly = false,
bool $excludeVendor = true,
Expand Down Expand Up @@ -116,13 +113,18 @@ public function check(
];

// Update summary
if ($isCompatible && !$hasWarnings) {
if ($isCompatible) {
$results['summary']['compatible']++;
} else {
$results['summary']['incompatible']++;
$results['hasIncompatibilities'] = true;
}

// Warnings alone still trigger the detail/recommendation display
if ($hasWarnings) {
$results['hasIncompatibilities'] = true;
}

if ($moduleInfo['isHyvaAware']) {
$results['summary']['hyvaAware']++;
}
Expand Down Expand Up @@ -195,19 +197,17 @@ public function formatResultsForDisplay(array $results, bool $showAll = false):
*/
private function getStatusDisplay(array $moduleData): string
{
if ($moduleData['moduleInfo']['isHyvaAware']) {
return '<fg=green>✓ Hyvä-Aware</>';
}
$hyvaTag = $moduleData['moduleInfo']['isHyvaAware'] ? ' (Hyvä-Aware)' : '';

if ($moduleData['compatible'] && !$moduleData['hasWarnings']) {
return '<fg=green>✓ Compatible</>';
return $moduleData['moduleInfo']['isHyvaAware'] ? '<fg=green>✓ Hyvä-Aware</>' : '<fg=green>✓ Compatible</>';
}

if ($moduleData['compatible']) {
return '<fg=yellow>⚠ Warnings</>';
return sprintf('<fg=yellow>⚠ Warnings%s</>', $hyvaTag);
}

return '<fg=red>✗ Incompatible</>';
return sprintf('<fg=red>✗ Incompatible%s</>', $hyvaTag);
}

/**
Expand Down Expand Up @@ -243,12 +243,11 @@ private function getIssuesDisplay(array $moduleData): string
/**
* Get detailed file issues for a module
*
* @param string $moduleName
* @param array $moduleData
* @phpstan-param ModuleEntry $moduleData
* @return array<int, array{file: string, issues: array<int, ScanIssue>}>
*/
public function getDetailedIssues(string $moduleName, array $moduleData): array
public function getDetailedIssues(array $moduleData): array
{
$scanResult = $moduleData['scanResult'];
$files = $scanResult['files'];
Expand Down
53 changes: 19 additions & 34 deletions src/Service/Hyva/IncompatibilityDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ class IncompatibilityDetector
'severity' => self::SEVERITY_CRITICAL,
],
[
'pattern' => '/ko\.observable|ko\.observableArray|ko\.computed/',
'pattern' => '/\bko\.(observable|observableArray|computed|pureComputed)\b/',
'description' => 'Knockout.js usage',
'severity' => self::SEVERITY_CRITICAL,
],
[
'pattern' => '/\bko\.(applyBindings|components|bindingHandlers)\b/',
'description' => 'Knockout.js usage',
'severity' => self::SEVERITY_CRITICAL,
],
Expand Down Expand Up @@ -78,6 +83,11 @@ class IncompatibilityDetector
'description' => 'data-mage-init JavaScript initialization',
'severity' => self::SEVERITY_CRITICAL,
],
[
'pattern' => '/data-bind\b\s*=/',
'description' => 'Knockout.js data-bind attribute',
'severity' => self::SEVERITY_CRITICAL,
],
[
'pattern' => '/x-magento-init/',
'description' => 'x-magento-init JavaScript initialization',
Expand All @@ -93,6 +103,11 @@ class IncompatibilityDetector
'description' => 'RequireJS in template',
'severity' => self::SEVERITY_CRITICAL,
],
[
'pattern' => '/<!--\s*ko\s/',
'description' => 'Knockout.js virtual element binding',
'severity' => self::SEVERITY_CRITICAL,
],
],
];

Expand Down Expand Up @@ -129,7 +144,7 @@ public function detectInFile(string $filePath): array
$lines = explode("\n", $content);

return $this->scanContentForPatterns($lines, self::INCOMPATIBLE_PATTERNS[$fileType]);
} catch (\Exception $e) {
} catch (\Throwable $e) {
return [];
}
}
Expand All @@ -145,7 +160,7 @@ private function mapExtensionToType(string $extension): string
return match ($extension) {
'js' => 'js',
'xml' => 'xml',
'phtml' => 'phtml',
'phtml', 'html' => 'phtml',
default => 'unknown',
};
}
Expand Down Expand Up @@ -180,43 +195,13 @@ private function scanContentForPatterns(array $lines, array $patterns): array
return $issues;
}

/**
* Get severity color for console output
*
* @param string $severity
* @return string
*/
public function getSeverityColor(string $severity): string
{
return match ($severity) {
self::SEVERITY_CRITICAL => 'red',
self::SEVERITY_WARNING => 'yellow',
default => 'white',
};
}

/**
* Get severity symbol
*
* @param string $severity
* @return string
*/
public function getSeveritySymbol(string $severity): string
{
return match ($severity) {
self::SEVERITY_CRITICAL => '✗',
self::SEVERITY_WARNING => '⚠',
default => 'ℹ',
};
}

/**
* Get file extension without using pathinfo().
*
* @param string $path
* @return string
*/
private function getExtensionFromPath(string $path): string
public function getExtensionFromPath(string $path): string
{
$normalized = str_replace('\\', '/', $path);
$trimmed = rtrim($normalized, '/');
Expand Down
Loading
Loading