From 89340979f7f5ab8e3fd7fcd4cd724521ad9b6037 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 01:00:34 -0400 Subject: [PATCH 01/20] Fix invalid data provider key in external URL tests The provider keyed data sets by URL, and an empty string is one of the internal URLs, producing an empty-string key. PHPUnit rejects this when listing tests, which broke any tooling relying on --list-tests. Co-Authored-By: Claude Opus 5 (1M context) --- tests/Facades/Concerns/ProvidesExternalUrls.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Facades/Concerns/ProvidesExternalUrls.php b/tests/Facades/Concerns/ProvidesExternalUrls.php index 4c0745c24d..e5471a05bd 100644 --- a/tests/Facades/Concerns/ProvidesExternalUrls.php +++ b/tests/Facades/Concerns/ProvidesExternalUrls.php @@ -140,7 +140,11 @@ private static function externalUrls() public static function externalUrlProvider() { $keyFn = function ($key) { - return is_null($key) ? 'null' : $key; + return match (true) { + is_null($key) => 'null', + $key === '' => 'empty string', + default => $key, + }; }; return [ From fce32f066353416c2784a732b7b6d64877431a83 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 01:00:41 -0400 Subject: [PATCH 02/20] Prevent ColorTest from leaking theme preferences setThemeColors() writes resource_path('preferences.yaml'), and while setUp() deleted it, nothing cleaned up after the last test. The final test's colors survived into unrelated tests that assert on preferences, making the suite order dependent. Delete the file in tearDown() too, matching DefaultPreferencesTest and PrecedenceTest. Co-Authored-By: Claude Opus 5 (1M context) --- tests/CP/ColorTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/CP/ColorTest.php b/tests/CP/ColorTest.php index 69a053705a..5182adfdd4 100644 --- a/tests/CP/ColorTest.php +++ b/tests/CP/ColorTest.php @@ -16,6 +16,12 @@ public function setUp(): void File::delete(resource_path('preferences.yaml')); } + public function tearDown(): void + { + File::delete(resource_path('preferences.yaml')); + parent::tearDown(); + } + #[Test] public function theme_has_defaults() { From e51f533d717a5190f449a464d966040e9d453dbe Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 01:00:45 -0400 Subject: [PATCH 03/20] Add Pest as the test runner Pest runs the existing PHPUnit test suite as-is, with no conversion to Pest syntax. It's added for its sharding support, which PHPUnit has no equivalent for. Co-Authored-By: Claude Opus 5 (1M context) --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4c50ba3c0..5bb18559af 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "laravel/socialite": "^5.28", "mockery/mockery": "^1.6.10", "orchestra/testbench": "^10.8 || ^11.0", + "pestphp/pest": "^4.7", "phpstan/phpstan": "^2.2", "phpunit/phpunit": "^12.5.23", "spatie/laravel-ray": "^1.43.6" @@ -67,7 +68,8 @@ "preferred-install": "dist", "sort-packages": true, "allow-plugins": { - "composer/package-versions-deprecated": true + "composer/package-versions-deprecated": true, + "pestphp/pest-plugin": true } }, "extra": { From 5b78d22927d57b804cd65f63a0044846037759e2 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 01:00:51 -0400 Subject: [PATCH 04/20] Shard PHP tests across four jobs in CI The test run is around 90% of each job's wall clock, so sharding is close to linear. Four shards takes the workflow from ~12.8 to ~4 minutes. Windows needs its own include entry per shard, since an include that overrides a matrix key creates a standalone combination rather than merging, and so wouldn't inherit the shard dimension. shards.json holds recorded timings so shards are balanced by duration rather than test count. Regenerate it with --update-shards. Adds a php-tests-result job that aggregates every shard, so branch protection can require one check instead of one per matrix cell. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/tests.yml | 38 +- tests/.pest/shards.json | 990 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1025 insertions(+), 3 deletions(-) create mode 100644 tests/.pest/shards.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aa2228fb55..3f96cbe602 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,13 +28,33 @@ jobs: laravel: [12.*, 13.*] stability: [prefer-lowest, prefer-stable] os: [ubuntu-latest] + shard: [1, 2, 3, 4] + # An include entry that overrides an existing matrix key (os) creates a standalone + # combination rather than merging, so it wouldn't inherit the shard dimension. + # Windows is the slowest job, so each shard has to be listed explicitly. include: - os: windows-latest php: 8.5 laravel: 12.* stability: prefer-stable + shard: 1 + - os: windows-latest + php: 8.5 + laravel: 12.* + stability: prefer-stable + shard: 2 + - os: windows-latest + php: 8.5 + laravel: 12.* + stability: prefer-stable + shard: 3 + - os: windows-latest + php: 8.5 + laravel: 12.* + stability: prefer-stable + shard: 4 - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} - shard ${{ matrix.shard }}/4 steps: - name: Checkout code @@ -101,7 +121,19 @@ jobs: - name: Execute tests if: steps.should-run-tests.outputs.result == 'true' - run: vendor/bin/phpunit + run: vendor/bin/pest --shard=${{ matrix.shard }}/4 --ci + + php-tests-result: + name: PHP tests + runs-on: ubuntu-latest + needs: [php-tests] + if: always() + permissions: {} + + steps: + - name: Fail if any shard failed + if: needs.php-tests.result != 'success' && needs.php-tests.result != 'skipped' + run: exit 1 js-tests: runs-on: ubuntu-latest @@ -159,7 +191,7 @@ jobs: slack: name: Slack Notification runs-on: ubuntu-latest - needs: [php-tests, js-tests] + needs: [php-tests-result, js-tests] permissions: actions: read # required by workflow-conclusion-action to determine overall workflow status if: always() diff --git a/tests/.pest/shards.json b/tests/.pest/shards.json new file mode 100644 index 0000000000..195d2fd9cb --- /dev/null +++ b/tests/.pest/shards.json @@ -0,0 +1,990 @@ +{ + "timings": { + "Tests\\API\\APITest": 1.0405, + "Tests\\API\\AuthenticationTest": 0.0588, + "Tests\\API\\CacherTest": 0.2441, + "Tests\\API\\ConfigTest": 0.535, + "Tests\\API\\FilterAuthorizerTest": 0.4326, + "Tests\\API\\ResourceAuthorizerTest": 0.5233, + "Tests\\Actions\\DeleteAssetFolderTest": 0.0943, + "Tests\\Actions\\DisableTwoFactorTest": 0.0458, + "Tests\\Actions\\DuplicateAssetTest": 0.0584, + "Tests\\Actions\\DuplicateEntryTest": 0.4994, + "Tests\\Actions\\DuplicateFormTest": 0.0328, + "Tests\\Actions\\DuplicateTermTest": 0.0563, + "Tests\\Actions\\ImpersonateTest": 0.2255, + "Tests\\Actions\\RenameAssetTest": 0.1195, + "Tests\\Addons\\AddonTest": 0.3589, + "Tests\\Addons\\FileSettingsRepositoryTest": 0.0529, + "Tests\\Addons\\FileSettingsTest": 0.0103, + "Tests\\Addons\\SettingsTest": 0.1689, + "Tests\\Antlers\\Components\\BladeComponentsTest": 0.1908, + "Tests\\Antlers\\Components\\ComponentScopeTest": 0.0253, + "Tests\\Antlers\\Components\\ComponentsCascadeTest": 0.0948, + "Tests\\Antlers\\Components\\SlotContentsTest": 0.0867, + "Tests\\Antlers\\ContentAllowlistConfigTest": 0.1073, + "Tests\\Antlers\\Parser\\AmbiguousTagPairTest": 0.0586, + "Tests\\Antlers\\Parser\\BasicNodeTest": 0.214, + "Tests\\Antlers\\Parser\\CommentsTest": 0.0235, + "Tests\\Antlers\\Parser\\ComponentTagsTest": 0.1501, + "Tests\\Antlers\\Parser\\ConditionalNodesTest": 0.0512, + "Tests\\Antlers\\Parser\\DirectivesTest": 0.2167, + "Tests\\Antlers\\Parser\\DocumentTransformerTest": 0.1181, + "Tests\\Antlers\\Parser\\IdentifierFinderTest": 0.0122, + "Tests\\Antlers\\Parser\\LogicGroupTest": 0.0129, + "Tests\\Antlers\\Parser\\ModifiersTest": 0.0848, + "Tests\\Antlers\\Parser\\NodeParametersTest": 0.2196, + "Tests\\Antlers\\Parser\\ParserErrorsTest": 0.397, + "Tests\\Antlers\\Parser\\PathParserTest": 0.0643, + "Tests\\Antlers\\Parser\\StringsTest": 0.0697, + "Tests\\Antlers\\Parser\\TernaryGroupsTest": 0.0277, + "Tests\\Antlers\\Parser\\VariableParsingTest": 0.013, + "Tests\\Antlers\\Runtime\\AntlersQueryBuilderTest": 0.1521, + "Tests\\Antlers\\Runtime\\ArithmeticTest": 0.1455, + "Tests\\Antlers\\Runtime\\ArraysTest": 0.3016, + "Tests\\Antlers\\Runtime\\AutomaticStatementTerminatorsTest": 0.0133, + "Tests\\Antlers\\Runtime\\ConditionLogicTest": 0.4335, + "Tests\\Antlers\\Runtime\\ConditionalFallbackTest": 0.0151, + "Tests\\Antlers\\Runtime\\ConditionalLogicValueTest": 0.0212, + "Tests\\Antlers\\Runtime\\ContentAllowListTest": 0.2239, + "Tests\\Antlers\\Runtime\\CoreModifiersTest": 0.6692, + "Tests\\Antlers\\Runtime\\DataRetrieverTest": 0.1679, + "Tests\\Antlers\\Runtime\\DeferredInterpolationTest": 0.0514, + "Tests\\Antlers\\Runtime\\EscapedLiteralsTest": 0.0133, + "Tests\\Antlers\\Runtime\\Fieldtypes\\ArrayFieldtypeTest": 0.0321, + "Tests\\Antlers\\Runtime\\Fieldtypes\\AssetTemplateTest": 0.0525, + "Tests\\Antlers\\Runtime\\Fieldtypes\\BardFieldtypeTest": 0.0616, + "Tests\\Antlers\\Runtime\\Fieldtypes\\ButtonGroupFieldtypeTest": 0.0122, + "Tests\\Antlers\\Runtime\\Fieldtypes\\CheckboxesFieldtypeTest": 0.016, + "Tests\\Antlers\\Runtime\\Fieldtypes\\CodeFieldtypeTest": 0.0255, + "Tests\\Antlers\\Runtime\\Fieldtypes\\ColorFieldtypeTest": 0.0122, + "Tests\\Antlers\\Runtime\\Fieldtypes\\FloatValFieldtypeTest": 0.013, + "Tests\\Antlers\\Runtime\\Fieldtypes\\GridFieldtypeTest": 0.0553, + "Tests\\Antlers\\Runtime\\Fieldtypes\\IntegerFieldtypeTest": 0.0143, + "Tests\\Antlers\\Runtime\\Fieldtypes\\LinkFieldtypeTest": 0.0134, + "Tests\\Antlers\\Runtime\\Fieldtypes\\MarkdownFieldtypeTest": 0.0394, + "Tests\\Antlers\\Runtime\\Fieldtypes\\RadioFieldtypeTest": 0.016, + "Tests\\Antlers\\Runtime\\Fieldtypes\\RangeFieldtypeTest": 0.0137, + "Tests\\Antlers\\Runtime\\Fieldtypes\\ReplicatorFieldtypeTest": 0.025, + "Tests\\Antlers\\Runtime\\Fieldtypes\\SelectFieldtypeTest": 0.0144, + "Tests\\Antlers\\Runtime\\Fieldtypes\\TableFieldtypeTest": 0.0173, + "Tests\\Antlers\\Runtime\\Fieldtypes\\TextareaFieldtypeTest": 0.0138, + "Tests\\Antlers\\Runtime\\Fieldtypes\\TimeFieldtypeTest": 0.0136, + "Tests\\Antlers\\Runtime\\Fieldtypes\\ToggleFieldtypeTest": 0.0137, + "Tests\\Antlers\\Runtime\\Fieldtypes\\VideoFieldtypeTest": 0.0135, + "Tests\\Antlers\\Runtime\\Fieldtypes\\YamlFieldtypeTest": 0.0183, + "Tests\\Antlers\\Runtime\\GroupByOperatorTest": 0.1828, + "Tests\\Antlers\\Runtime\\InterpolationTest": 0.0561, + "Tests\\Antlers\\Runtime\\LogicGroupTest": 0.0127, + "Tests\\Antlers\\Runtime\\LoopTest": 0.1086, + "Tests\\Antlers\\Runtime\\MarkdownContentTest": 0.0137, + "Tests\\Antlers\\Runtime\\MethodCallTest": 0.2104, + "Tests\\Antlers\\Runtime\\MethodStyleModifiersTest": 0.0626, + "Tests\\Antlers\\Runtime\\ModelTest": 0.0903, + "Tests\\Antlers\\Runtime\\ModifierArgumentTest": 0.0303, + "Tests\\Antlers\\Runtime\\ModifierEquivalenceTest": 0.0739, + "Tests\\Antlers\\Runtime\\NamedSlotsTest": 0.0625, + "Tests\\Antlers\\Runtime\\NoparseTest": 0.0481, + "Tests\\Antlers\\Runtime\\NullCoalescenceTest": 0.0511, + "Tests\\Antlers\\Runtime\\OnceTest": 0.0388, + "Tests\\Antlers\\Runtime\\OrderbyOperatorTest": 0.2145, + "Tests\\Antlers\\Runtime\\ParameterStyleModifierTest": 0.0384, + "Tests\\Antlers\\Runtime\\ParametersTest": 0.2449, + "Tests\\Antlers\\Runtime\\ParserIsolationTest": 0.183, + "Tests\\Antlers\\Runtime\\PartialsTest": 0.0656, + "Tests\\Antlers\\Runtime\\PhpDisabledTest": 0.1208, + "Tests\\Antlers\\Runtime\\PhpEnabledTest": 0.2459, + "Tests\\Antlers\\Runtime\\PrefixedFieldsTest": 0.1139, + "Tests\\Antlers\\Runtime\\PreparserTest": 0.0166, + "Tests\\Antlers\\Runtime\\RecursiveNodesTest": 0.2375, + "Tests\\Antlers\\Runtime\\RuntimeConfigurationTest": 0.0131, + "Tests\\Antlers\\Runtime\\RuntimeValuesTest": 0.0406, + "Tests\\Antlers\\Runtime\\StackedSectionsTest": 0.0138, + "Tests\\Antlers\\Runtime\\StacksTest": 0.202, + "Tests\\Antlers\\Runtime\\StressTest": 0.1613, + "Tests\\Antlers\\Runtime\\StrictNullCoalescenceTest": 0.1669, + "Tests\\Antlers\\Runtime\\StrictVariablesTest": 0.0367, + "Tests\\Antlers\\Runtime\\StringsTest": 0.0618, + "Tests\\Antlers\\Runtime\\StructuresTest": 0.0116, + "Tests\\Antlers\\Runtime\\TagCheckScopeTest": 0.05, + "Tests\\Antlers\\Runtime\\TagsTest": 0.026, + "Tests\\Antlers\\Runtime\\TemplateTest": 2.1243, + "Tests\\Antlers\\Runtime\\TupleListTest": 0.0299, + "Tests\\Antlers\\Runtime\\UnlessTest": 0.0249, + "Tests\\Antlers\\Runtime\\UseCaseTests\\ConditionalParameterTest": 0.0149, + "Tests\\Antlers\\Runtime\\ValueInterpolationTest": 0.0551, + "Tests\\Antlers\\Runtime\\VariablePriorityTest": 0.1912, + "Tests\\Antlers\\Runtime\\VariableVariablesTest": 0.0311, + "Tests\\Antlers\\Runtime\\VariablesTest": 0.0389, + "Tests\\Antlers\\Runtime\\VoidParametersTest": 0.0177, + "Tests\\Antlers\\Runtime\\WhereOperatorTest": 0.1015, + "Tests\\Antlers\\Runtime\\YieldTest": 0.099, + "Tests\\Antlers\\Sandbox\\AugmentedAssignmentTest": 0.0468, + "Tests\\Antlers\\Sandbox\\BitwiseOperatorsTest": 0.077, + "Tests\\Antlers\\Sandbox\\ConditionalsTest": 0.1655, + "Tests\\Antlers\\Sandbox\\LanguageOperatorTest": 0.0681, + "Tests\\Antlers\\Sandbox\\VariableAssignmentTest": 0.4268, + "Tests\\Antlers\\ScratchTest": 0.1002, + "Tests\\Assets\\AssetContainerTest": 0.7759, + "Tests\\Assets\\AssetFolderTest": 0.5988, + "Tests\\Assets\\AssetRepositoryTest": 0.0718, + "Tests\\Assets\\AssetTest": 2.2326, + "Tests\\Assets\\AssetUploaderTest": 0.1918, + "Tests\\Assets\\AttributesTest": 0.8409, + "Tests\\Assets\\CropAspectRatiosTest": 0.0685, + "Tests\\Assets\\ExtractInfoTest": 0.0156, + "Tests\\Assets\\ReplacementFileTest": 0.0381, + "Tests\\Auth\\AugmentedUserTest": 0.016, + "Tests\\Auth\\CpForgotPasswordTest": 0.3168, + "Tests\\Auth\\ElevatedSessionDisabledTest": 0.2369, + "Tests\\Auth\\ElevatedSessionTest": 5.6138, + "Tests\\Auth\\EloquentUserRepositoryTest": 0.7206, + "Tests\\Auth\\Eloquent\\EloquentRoleTest": 3.8851, + "Tests\\Auth\\Eloquent\\EloquentUserGroupTest": 2.2027, + "Tests\\Auth\\Eloquent\\EloquentUserQueryBuilderTest": 2.1323, + "Tests\\Auth\\Eloquent\\EloquentUserTest": 4.3197, + "Tests\\Auth\\FileUserTest": 1.1383, + "Tests\\Auth\\ForgotPasswordTest": 10.0111, + "Tests\\Auth\\HasAvatarTest": 0.0918, + "Tests\\Auth\\LoginTest": 1.1843, + "Tests\\Auth\\Protect\\AuthenticationProtectionTest": 0.0634, + "Tests\\Auth\\Protect\\CustomProtectionTest": 0.0208, + "Tests\\Auth\\Protect\\FallbackProtectorTest": 0.0117, + "Tests\\Auth\\Protect\\IpProtectorTest": 0.1159, + "Tests\\Auth\\Protect\\NoProtectionTest": 0.02, + "Tests\\Auth\\Protect\\PasswordEntryTest": 0.1544, + "Tests\\Auth\\Protect\\PasswordProtectionTest": 0.1664, + "Tests\\Auth\\Protect\\ProtectionTest": 0.1469, + "Tests\\Auth\\ResetPasswordTest": 4.6422, + "Tests\\Auth\\RoleRepositoryTest": 0.0574, + "Tests\\Auth\\RoleTest": 0.1415, + "Tests\\Auth\\StacheUserRepositoryTest": 0.5659, + "Tests\\Auth\\TokenRepositoryTest": 0.3478, + "Tests\\Auth\\TwoFactor\\GenerateNewRecoveryCodesTest": 0.019, + "Tests\\Auth\\TwoFactor\\RecoveryCodeTest": 0.0232, + "Tests\\Auth\\UserGroupTest": 0.2487, + "Tests\\Auth\\WebAuthn\\EloquentPasskeyTest": 4.003, + "Tests\\Auth\\WebAuthn\\FilePasskeyTest": 0.1572, + "Tests\\Auth\\WebAuthn\\PasskeyTest": 0.0886, + "Tests\\Auth\\WebAuthn\\WebAuthnTest": 0.1147, + "Tests\\CP\\AuthRedirectTest": 0.1084, + "Tests\\CP\\Breadcrumbs\\BreadcrumbsTest": 0.3386, + "Tests\\CP\\CarbonAsVueComponentTest": 0.0228, + "Tests\\CP\\ColorTest": 0.1008, + "Tests\\CP\\ColumnTest": 0.0394, + "Tests\\CP\\ColumnsTest": 0.0554, + "Tests\\CP\\CpTest": 0.0224, + "Tests\\CP\\EditRedirectControllerTest": 0.0546, + "Tests\\CP\\LivePreviewTest": 0.0246, + "Tests\\CP\\Navigation\\ActiveNavItemTest": 1.0118, + "Tests\\CP\\Navigation\\CoreNavTest": 0.2664, + "Tests\\CP\\Navigation\\NavPreferencesNormalizerTest": 0.3181, + "Tests\\CP\\Navigation\\NavPreferencesTest": 1.0974, + "Tests\\CP\\Navigation\\NavTest": 0.6951, + "Tests\\CP\\Navigation\\NavTransformerTest": 0.7935, + "Tests\\CP\\StartPageTest": 0.0395, + "Tests\\CP\\Toasts\\ManagerTest": 0.0136, + "Tests\\CP\\Utilities\\UtilityRepositoryTest": 0.0503, + "Tests\\CommandPalette\\CommandPaletteTest": 0.0584, + "Tests\\CommandPalette\\ContentSearchTest": 0.019, + "Tests\\Composer\\AddonChangelogTest": 0.094, + "Tests\\Composer\\ComposerJsonTest": 0.0568, + "Tests\\Composer\\ComposerLockBackupTest": 0.0019, + "Tests\\Composer\\ComposerLockTest": 0.1653, + "Tests\\Composer\\ComposerTest": 16.3542, + "Tests\\Composer\\CoreChangelogTest": 0.0716, + "Tests\\Console\\Commands\\AssetsGeneratePresetsTest": 0.1734, + "Tests\\Console\\Commands\\AssetsMetaCleanTest": 0.1311, + "Tests\\Console\\Commands\\AssetsMetaTest": 0.5655, + "Tests\\Console\\Commands\\MakeActionTest": 0.0871, + "Tests\\Console\\Commands\\MakeAddonTest": 2.2786, + "Tests\\Console\\Commands\\MakeDictionaryTest": 0.0639, + "Tests\\Console\\Commands\\MakeFieldtypeTest": 0.1068, + "Tests\\Console\\Commands\\MakeFilterTest": 0.0638, + "Tests\\Console\\Commands\\MakeModifierTest": 0.0585, + "Tests\\Console\\Commands\\MakeScopeTest": 0.0632, + "Tests\\Console\\Commands\\MakeTagTest": 0.0637, + "Tests\\Console\\Commands\\MakeUserTest": 0.6063, + "Tests\\Console\\Commands\\MakeWidgetTest": 0.1046, + "Tests\\Console\\Commands\\MigrateDatesToUtcTest": 1.4229, + "Tests\\Console\\Commands\\ProEnableTest": 0.1727, + "Tests\\Console\\Commands\\SetupCpViteTest": 0.2014, + "Tests\\Console\\Commands\\SiteClearTest": 0.0127, + "Tests\\Console\\Commands\\StacheClearTest": 0.0737, + "Tests\\Console\\Commands\\StacheRefreshTest": 0.0889, + "Tests\\Console\\Commands\\StacheWarmTest": 0.0425, + "Tests\\Console\\Commands\\StaticWarmJobTest": 0.0678, + "Tests\\Console\\Commands\\StaticWarmTest": 0.6553, + "Tests\\Console\\Commands\\StaticWarmUncachedJobTest": 0.0218, + "Tests\\Console\\FfmpegTest": 0.0166, + "Tests\\Console\\NullConsoleTest": 0.0203, + "Tests\\Console\\PleaseTest": 0.0573, + "Tests\\Console\\ProcessTest": 0.1317, + "Tests\\Data\\Assets\\AssetQueryBuilderTest": 0.8705, + "Tests\\Data\\AugmentedCollectionTest": 0.0925, + "Tests\\Data\\AugmentedTest": 0.1183, + "Tests\\Data\\DataCollectionTest": 0.0412, + "Tests\\Data\\DataRepositoryTest": 1.9697, + "Tests\\Data\\Entries\\AugmentedEntryTest": 0.1078, + "Tests\\Data\\Entries\\CollectionTest": 0.7553, + "Tests\\Data\\Entries\\EntryQueryBuilderTest": 2.4495, + "Tests\\Data\\Entries\\EntryTest": 2.3802, + "Tests\\Data\\Entries\\GetDateFromPathTest": 0.1126, + "Tests\\Data\\Entries\\GetSlugFromPathTest": 0.1291, + "Tests\\Data\\Entries\\GetSuffixFromPathTest": 0.1108, + "Tests\\Data\\Entries\\RemoveSuffixFromPathTest": 0.1042, + "Tests\\Data\\Entries\\ScheduledEntriesTest": 0.0711, + "Tests\\Data\\ExistsAsFileTest": 0.048, + "Tests\\Data\\Globals\\GlobalSetTest": 0.2401, + "Tests\\Data\\Globals\\VariablesTest": 0.1828, + "Tests\\Data\\HasAugmentedDataTest": 0.0166, + "Tests\\Data\\HasAugmentedInstanceTest": 0.0526, + "Tests\\Data\\StoresComputedFieldCallbacksTest": 0.0252, + "Tests\\Data\\StoresScopedComputedFieldCallbacksTest": 0.0424, + "Tests\\Data\\Structures\\AugmentedPageTest": 0.0548, + "Tests\\Data\\Structures\\BranchIdsTest": 0.014, + "Tests\\Data\\Structures\\CollectionStructureTest": 0.2242, + "Tests\\Data\\Structures\\CollectionTreeDiffTest": 0.1342, + "Tests\\Data\\Structures\\CollectionTreeTest": 0.1671, + "Tests\\Data\\Structures\\NavTest": 0.2953, + "Tests\\Data\\Structures\\NavTreeTest": 0.1054, + "Tests\\Data\\Structures\\PageTest": 0.3388, + "Tests\\Data\\Structures\\PagesTest": 0.0212, + "Tests\\Data\\Structures\\StructureRepositoryTest": 0.0462, + "Tests\\Data\\Structures\\TreeTest": 0.2803, + "Tests\\Data\\Taxonomies\\AugmentedTermTest": 0.0698, + "Tests\\Data\\Taxonomies\\LocalizedTermTest": 0.1761, + "Tests\\Data\\Taxonomies\\TaxonomyTest": 0.4141, + "Tests\\Data\\Taxonomies\\TermQueryBuilderTest": 0.7669, + "Tests\\Data\\Taxonomies\\TermTest": 0.3117, + "Tests\\Data\\Taxonomies\\ViewsTest": 0.7065, + "Tests\\Data\\TracksLastModifiedTest": 0.1607, + "Tests\\Data\\Users\\UserQueryBuilderTest": 0.4703, + "Tests\\Dictionaries\\CountriesTest": 0.07, + "Tests\\Dictionaries\\CurrenciesTest": 0.0761, + "Tests\\Dictionaries\\DictionaryRepositoryTest": 0.0437, + "Tests\\Dictionaries\\FileTest": 0.1593, + "Tests\\Dictionaries\\ItemTest": 0.0123, + "Tests\\Dictionaries\\LocalesTest": 0.025, + "Tests\\Dictionaries\\TimezonesTest": 0.0898, + "Tests\\Events\\MacroTest": 0.0433, + "Tests\\Events\\SubscriberTest": 0.0506, + "Tests\\Exceptions\\ControlPanelExceptionHandlerTest": 0.0123, + "Tests\\Extensions\\FileStoreTest": 0.0165, + "Tests\\Facades\\ConfigTest": 0.1891, + "Tests\\Facades\\ParseTest": 0.1752, + "Tests\\Facades\\PathTest": 0.0559, + "Tests\\Facades\\PatternTest": 0.3067, + "Tests\\Facades\\UrlTest": 6.7571, + "Tests\\Feature\\Addons\\EditAddonSettingsTest": 0.7248, + "Tests\\Feature\\Addons\\UpdateAddonSettingsTest": 0.1669, + "Tests\\Feature\\Addons\\ViewAddonListingTest": 1.0468, + "Tests\\Feature\\AssetContainers\\EditAssetContainerTest": 0.0831, + "Tests\\Feature\\AssetContainers\\UpdateAssetContainerTest": 0.0627, + "Tests\\Feature\\Assets\\AssetIndexTest": 0.0862, + "Tests\\Feature\\Assets\\BrowserTest": 0.5652, + "Tests\\Feature\\Assets\\ClearAssetGlideCacheTest": 0.0863, + "Tests\\Feature\\Assets\\CropAssetTest": 0.5914, + "Tests\\Feature\\Assets\\DownloadAssetTest": 0.0734, + "Tests\\Feature\\Assets\\ImageThumbnailTest": 0.0793, + "Tests\\Feature\\Assets\\PdfThumbnailTest": 0.0733, + "Tests\\Feature\\Assets\\ReuploadAssetTest": 0.0429, + "Tests\\Feature\\Assets\\ShowAssetTest": 0.0696, + "Tests\\Feature\\Assets\\StoreAssetTest": 0.4724, + "Tests\\Feature\\Assets\\SvgThumbnailTest": 0.0725, + "Tests\\Feature\\Auth\\DeletePasskeyTest": 0.2169, + "Tests\\Feature\\Auth\\PasskeyLoginTest": 0.2754, + "Tests\\Feature\\Auth\\StorePasskeyTest": 0.3902, + "Tests\\Feature\\AuthenticationTest": 0.0641, + "Tests\\Feature\\Blueprints\\EditCustomBlueprintTest": 0.0628, + "Tests\\Feature\\Blueprints\\StoreCustomBlueprintTest": 0.0403, + "Tests\\Feature\\Blueprints\\ViewBlueprintListingTest": 0.1017, + "Tests\\Feature\\Collections\\Blueprints\\CreateBlueprintTest": 0.0595, + "Tests\\Feature\\Collections\\Blueprints\\EditBlueprintTest": 0.0659, + "Tests\\Feature\\Collections\\Blueprints\\StoreBlueprintTest": 0.092, + "Tests\\Feature\\Collections\\Blueprints\\UpdateBlueprintTest": 0.1566, + "Tests\\Feature\\Collections\\Blueprints\\ViewBlueprintListingTest": 0.0605, + "Tests\\Feature\\Collections\\CreateCollectionTest": 0.0632, + "Tests\\Feature\\Collections\\DeleteCollectionTest": 0.1329, + "Tests\\Feature\\Collections\\EditCollectionTest": 0.0721, + "Tests\\Feature\\Collections\\ShowRegularCollectionTest": 0.1141, + "Tests\\Feature\\Collections\\ShowStructuredCollectionTest": 0.1115, + "Tests\\Feature\\Collections\\StoreCollectionTest": 0.1046, + "Tests\\Feature\\Collections\\UpdateCollectionTest": 0.1507, + "Tests\\Feature\\Collections\\UpdateCollectionTreeTest": 0.3405, + "Tests\\Feature\\Collections\\ViewCollectionListingTest": 0.3207, + "Tests\\Feature\\Entries\\AddsHeadersToLivePreviewTest": 0.0539, + "Tests\\Feature\\Entries\\CreateEntryTest": 0.2106, + "Tests\\Feature\\Entries\\DeleteEntryTest": 0.0791, + "Tests\\Feature\\Entries\\EditEntryTest": 0.0559, + "Tests\\Feature\\Entries\\EntryRevisionsTest": 0.4031, + "Tests\\Feature\\Entries\\GetByTaxonomyTermsTest": 0.0428, + "Tests\\Feature\\Entries\\LocalizeEntryTest": 0.3385, + "Tests\\Feature\\Entries\\MountingTest": 0.0936, + "Tests\\Feature\\Entries\\PreviewEntryTest": 0.1226, + "Tests\\Feature\\Entries\\ReorderEntriesTest": 0.1126, + "Tests\\Feature\\Entries\\StoreEntryTest": 0.4795, + "Tests\\Feature\\Entries\\SubstitutesEntryForLivePreviewTest": 0.0708, + "Tests\\Feature\\Entries\\UpdateEntryTest": 0.5764, + "Tests\\Feature\\Entries\\ViewEntryListingTest": 0.064, + "Tests\\Feature\\Fields\\MetaControllerTest": 0.2217, + "Tests\\Feature\\Fieldsets\\CreateFieldsetTest": 0.0276, + "Tests\\Feature\\Fieldsets\\EditFieldsetTest": 0.0962, + "Tests\\Feature\\Fieldsets\\StoreFieldsetTest": 0.1247, + "Tests\\Feature\\Fieldsets\\UpdateFieldsetTest": 0.2007, + "Tests\\Feature\\Fieldsets\\ViewFieldsetListingTest": 0.0631, + "Tests\\Feature\\Fieldtypes\\AssetsFieldtypeControllerTest": 0.0901, + "Tests\\Feature\\Fieldtypes\\FilesTest": 0.2815, + "Tests\\Feature\\Fieldtypes\\PreviewMarkdownTest": 0.0622, + "Tests\\Feature\\Fieldtypes\\RelationshipFieldtypeTest": 0.7141, + "Tests\\Feature\\Fieldtypes\\ReplicatorSetControllerAuthorizationTest": 0.1012, + "Tests\\Feature\\Forms\\CreateFormTest": 0.0574, + "Tests\\Feature\\Forms\\DeleteFormTest": 0.0372, + "Tests\\Feature\\Forms\\EditFormTest": 0.1052, + "Tests\\Feature\\Forms\\StoreFormTest": 0.1087, + "Tests\\Feature\\Forms\\UpdateFormTest": 0.1022, + "Tests\\Feature\\Forms\\ViewSubmissionsListingTest": 0.0257, + "Tests\\Feature\\Globals\\ConfigureGlobalsTest": 0.0686, + "Tests\\Feature\\Globals\\EditGlobalVariablesTest": 0.1628, + "Tests\\Feature\\Globals\\UpdateGlobalVariablesTest": 0.1149, + "Tests\\Feature\\Globals\\UpdateGlobalsTest": 0.0801, + "Tests\\Feature\\Globals\\ViewGlobalsListingTest": 0.1469, + "Tests\\Feature\\GraphQL\\AssetContainerTest": 0.0814, + "Tests\\Feature\\GraphQL\\AssetContainersTest": 0.0584, + "Tests\\Feature\\GraphQL\\AssetTest": 0.185, + "Tests\\Feature\\GraphQL\\AssetsTest": 0.2412, + "Tests\\Feature\\GraphQL\\AuthenticationTest": 0.075, + "Tests\\Feature\\GraphQL\\CollectionTest": 0.1127, + "Tests\\Feature\\GraphQL\\CollectionsTest": 0.0584, + "Tests\\Feature\\GraphQL\\CustomMiddlewareTest": 0.0489, + "Tests\\Feature\\GraphQL\\CustomMutationTest": 0.0521, + "Tests\\Feature\\GraphQL\\CustomQueryTest": 0.0447, + "Tests\\Feature\\GraphQL\\DisablesRoutesTest": 0.013, + "Tests\\Feature\\GraphQL\\EntriesTest": 0.8189, + "Tests\\Feature\\GraphQL\\EntryTest": 0.7624, + "Tests\\Feature\\GraphQL\\Fieldtypes\\ArrFieldtypeTest": 0.0275, + "Tests\\Feature\\GraphQL\\Fieldtypes\\AssetsFieldtypeTest": 0.0465, + "Tests\\Feature\\GraphQL\\Fieldtypes\\BardFieldtypeTest": 0.1807, + "Tests\\Feature\\GraphQL\\Fieldtypes\\ButtonGroupFieldtypeTest": 0.0271, + "Tests\\Feature\\GraphQL\\Fieldtypes\\CheckboxesFieldtypeTest": 0.0206, + "Tests\\Feature\\GraphQL\\Fieldtypes\\CodeFieldtypeTest": 0.0209, + "Tests\\Feature\\GraphQL\\Fieldtypes\\CollectionsFieldtypeTest": 0.0514, + "Tests\\Feature\\GraphQL\\Fieldtypes\\DateFieldtypeTest": 0.0413, + "Tests\\Feature\\GraphQL\\Fieldtypes\\DictionaryFieldtypeTest": 0.1068, + "Tests\\Feature\\GraphQL\\Fieldtypes\\EntriesFieldtypeTest": 0.0486, + "Tests\\Feature\\GraphQL\\Fieldtypes\\FloatvalFieldtypeTest": 0.0194, + "Tests\\Feature\\GraphQL\\Fieldtypes\\GridFieldtypeTest": 0.0749, + "Tests\\Feature\\GraphQL\\Fieldtypes\\IntegerFieldtypeTest": 0.0201, + "Tests\\Feature\\GraphQL\\Fieldtypes\\LinkFieldtypeTest": 0.1071, + "Tests\\Feature\\GraphQL\\Fieldtypes\\ListFieldtypeTest": 0.0271, + "Tests\\Feature\\GraphQL\\Fieldtypes\\MarkdownFieldtypeTest": 0.0203, + "Tests\\Feature\\GraphQL\\Fieldtypes\\RadioFieldtypeTest": 0.0215, + "Tests\\Feature\\GraphQL\\Fieldtypes\\RangeFieldtypeTest": 0.0474, + "Tests\\Feature\\GraphQL\\Fieldtypes\\ReplicatorFieldtypeTest": 0.1016, + "Tests\\Feature\\GraphQL\\Fieldtypes\\SelectFieldtypeTest": 0.0466, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TableFieldtypeTest": 0.0195, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TaggableFieldtypeTest": 0.0195, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TaxonomiesFieldtypeTest": 0.0497, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TemplateFieldtypeTest": 0.0194, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TermsFieldtypeTest": 0.0532, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TextFieldtypeTest": 0.0185, + "Tests\\Feature\\GraphQL\\Fieldtypes\\TimeFieldtypeTest": 0.0194, + "Tests\\Feature\\GraphQL\\Fieldtypes\\ToggleFieldtypeTest": 0.0191, + "Tests\\Feature\\GraphQL\\Fieldtypes\\UserGroupsFieldtypeTest": 0.0444, + "Tests\\Feature\\GraphQL\\Fieldtypes\\UserRolesFieldtypeTest": 0.0386, + "Tests\\Feature\\GraphQL\\Fieldtypes\\UsersFieldtypeTest": 0.0506, + "Tests\\Feature\\GraphQL\\Fieldtypes\\VideoFieldtypeTest": 0.0197, + "Tests\\Feature\\GraphQL\\Fieldtypes\\YamlFieldtypeTest": 0.0259, + "Tests\\Feature\\GraphQL\\FormTest": 0.1223, + "Tests\\Feature\\GraphQL\\FormsTest": 0.0552, + "Tests\\Feature\\GraphQL\\GlobalTest": 0.1031, + "Tests\\Feature\\GraphQL\\GlobalsTest": 0.0589, + "Tests\\Feature\\GraphQL\\HandlesTokensTest": 0.0131, + "Tests\\Feature\\GraphQL\\NavTest": 0.1704, + "Tests\\Feature\\GraphQL\\NavsTest": 0.0531, + "Tests\\Feature\\GraphQL\\PageTest": 0.0239, + "Tests\\Feature\\GraphQL\\PingPongTest": 0.0137, + "Tests\\Feature\\GraphQL\\QueryAuthorizationTest": 0.0441, + "Tests\\Feature\\GraphQL\\RequestCacheTest": 0.1708, + "Tests\\Feature\\GraphQL\\ResolvesValuesTest": 0.0533, + "Tests\\Feature\\GraphQL\\SitesTest": 0.0361, + "Tests\\Feature\\GraphQL\\StatamicProRequiredTest": 0.0124, + "Tests\\Feature\\GraphQL\\TaxonomiesTest": 0.0515, + "Tests\\Feature\\GraphQL\\TaxonomyTest": 0.0443, + "Tests\\Feature\\GraphQL\\TermTest": 0.138, + "Tests\\Feature\\GraphQL\\TermsTest": 0.4195, + "Tests\\Feature\\GraphQL\\UserTest": 0.106, + "Tests\\Feature\\GraphQL\\UsersTest": 0.3283, + "Tests\\Feature\\InertiaRootViewTest": 0.0126, + "Tests\\Feature\\Navigation\\CreateNavigationPageTest": 0.1271, + "Tests\\Feature\\Navigation\\CreateNavigationTest": 0.0581, + "Tests\\Feature\\Navigation\\DeleteNavigationTest": 0.0413, + "Tests\\Feature\\Navigation\\EditNavigationPageTest": 0.1535, + "Tests\\Feature\\Navigation\\EditNavigationTest": 0.059, + "Tests\\Feature\\Navigation\\StoreNavigationTest": 0.0843, + "Tests\\Feature\\Navigation\\UpdateNavigationPageTest": 0.0419, + "Tests\\Feature\\Navigation\\UpdateNavigationTest": 0.0971, + "Tests\\Feature\\Navigation\\UpdateNavigationTreeTest": 0.0938, + "Tests\\Feature\\Navigation\\ViewNavigationListingTest": 0.2335, + "Tests\\Feature\\RateLimitingTest": 0.8529, + "Tests\\Feature\\Revisions\\RevisionsTest": 0.0273, + "Tests\\Feature\\Roles\\StoreRoleTest": 0.153, + "Tests\\Feature\\Roles\\UpdateRoleTest": 0.1697, + "Tests\\Feature\\Sites\\SelectSiteTest": 0.0661, + "Tests\\Feature\\SlugTest": 0.2763, + "Tests\\Feature\\Taxonomies\\Blueprints\\CreateBlueprintTest": 0.0596, + "Tests\\Feature\\Taxonomies\\Blueprints\\EditBlueprintTest": 0.0638, + "Tests\\Feature\\Taxonomies\\Blueprints\\StoreBlueprintTest": 0.0853, + "Tests\\Feature\\Taxonomies\\Blueprints\\UpdateBlueprintTest": 0.1224, + "Tests\\Feature\\Taxonomies\\Blueprints\\ViewBlueprintListingTest": 0.0543, + "Tests\\Feature\\Taxonomies\\TermEntriesTest": 0.1248, + "Tests\\Feature\\Taxonomies\\UpdateTaxonomyTest": 0.0736, + "Tests\\Feature\\Taxonomies\\UpdateTermTest": 0.0697, + "Tests\\Feature\\Taxonomies\\ViewTermsListingTest": 0.0757, + "Tests\\Feature\\UserGroups\\StoreGroupTest": 0.0981, + "Tests\\Feature\\UserGroups\\UpdateGroupTest": 0.1011, + "Tests\\Feature\\Users\\CreateUserTest": 0.0585, + "Tests\\Feature\\Users\\DisableTwoFactorTest": 0.0687, + "Tests\\Feature\\Users\\EditUserTest": 0.1853, + "Tests\\Feature\\Users\\EnableTwoFactorTest": 0.2663, + "Tests\\Feature\\Users\\StoreUserTest": 0.2381, + "Tests\\Feature\\Users\\TwoFactorChallengeTest": 0.1991, + "Tests\\Feature\\Users\\TwoFactorRecoveryCodesTest": 0.1866, + "Tests\\Feature\\Users\\TwoFactorRoutesTest": 0.204, + "Tests\\Feature\\Users\\TwoFactorSetupTest": 0.166, + "Tests\\Feature\\Users\\UpdateUserTest": 0.1424, + "Tests\\Feature\\Users\\UserExistsTest": 0.0836, + "Tests\\Feature\\Users\\UserRegistrationTest": 0.2326, + "Tests\\Fields\\ArrayableStringTest": 0.0687, + "Tests\\Fields\\BlueprintRepositoryTest": 0.2285, + "Tests\\Fields\\BlueprintTest": 0.6025, + "Tests\\Fields\\ClassRuleParserTest": 0.1793, + "Tests\\Fields\\ConfigFieldsTest": 0.0109, + "Tests\\Fields\\FieldRepositoryTest": 0.052, + "Tests\\Fields\\FieldTest": 0.3638, + "Tests\\Fields\\FieldTransformerTest": 0.1804, + "Tests\\Fields\\FieldsTest": 0.4531, + "Tests\\Fields\\FieldsetRepositoryTest": 0.207, + "Tests\\Fields\\FieldsetTest": 0.515, + "Tests\\Fields\\FieldtypeRepositoryTest": 0.0657, + "Tests\\Fields\\FieldtypeTest": 0.5123, + "Tests\\Fields\\LabeledValueTest": 0.0441, + "Tests\\Fields\\SectionTest": 0.0623, + "Tests\\Fields\\TabTest": 0.1242, + "Tests\\Fields\\ValidatorTest": 0.1547, + "Tests\\Fields\\ValueTest": 0.2806, + "Tests\\Fields\\ValuesTest": 0.5012, + "Tests\\Fieldtypes\\ArrayTest": 0.7081, + "Tests\\Fieldtypes\\AssetsTest": 0.4342, + "Tests\\Fieldtypes\\BardTest": 0.7992, + "Tests\\Fieldtypes\\ButtonGroupTest": 0.2824, + "Tests\\Fieldtypes\\CheckboxesTest": 0.2478, + "Tests\\Fieldtypes\\CodeTest": 0.1389, + "Tests\\Fieldtypes\\Concerns\\ResolvesStatamicUrlsTest": 0.076, + "Tests\\Fieldtypes\\DateTest": 1.3935, + "Tests\\Fieldtypes\\DictionaryFieldsTest": 0.1042, + "Tests\\Fieldtypes\\DictionaryTest": 0.4069, + "Tests\\Fieldtypes\\EntriesTest": 1.375, + "Tests\\Fieldtypes\\GridTest": 0.1565, + "Tests\\Fieldtypes\\IconTest": 0.0348, + "Tests\\Fieldtypes\\LinkTest": 0.5558, + "Tests\\Fieldtypes\\ListTest": 0.0309, + "Tests\\Fieldtypes\\MarkdownTest": 0.1484, + "Tests\\Fieldtypes\\NestedFieldsTest": 0.0365, + "Tests\\Fieldtypes\\RadioTest": 0.2412, + "Tests\\Fieldtypes\\RangeFieldtypeTest": 0.12, + "Tests\\Fieldtypes\\ReplicatorTest": 0.4044, + "Tests\\Fieldtypes\\SelectTest": 0.4008, + "Tests\\Fieldtypes\\SetsTest": 0.0957, + "Tests\\Fieldtypes\\TaggableTest": 0.0112, + "Tests\\Fieldtypes\\TemplateFolderTest": 0.0165, + "Tests\\Fieldtypes\\TemplatesTest": 0.0339, + "Tests\\Fieldtypes\\TermsTest": 1.2434, + "Tests\\Fieldtypes\\TextTest": 0.1235, + "Tests\\Fieldtypes\\TimeTest": 0.219, + "Tests\\Fieldtypes\\ToggleTest": 0.0333, + "Tests\\Fieldtypes\\UserGroupsTest": 0.041, + "Tests\\Fieldtypes\\UserRolesTest": 0.0326, + "Tests\\Fieldtypes\\UsersTest": 0.4733, + "Tests\\Fieldtypes\\WidthTest": 0.0996, + "Tests\\Fieldtypes\\YamlTest": 0.1175, + "Tests\\Filesystem\\FilesystemAdapterTest": 0.5365, + "Tests\\Filesystem\\FlysystemAdapterTest": 0.507, + "Tests\\Filesystem\\ManagerTest": 0.012, + "Tests\\FluentlyGetsAndSetsTest": 0.0762, + "Tests\\Forms\\CsvExporterTest": 0.0171, + "Tests\\Forms\\EmailTest": 1.0725, + "Tests\\Forms\\FormRepositoryTest": 0.0463, + "Tests\\Forms\\FormTest": 0.1784, + "Tests\\Forms\\SendEmailTest": 0.0136, + "Tests\\Forms\\SendEmailsTest": 0.0964, + "Tests\\Forms\\SubmissionQueryBuilderTest": 0.4412, + "Tests\\Forms\\SubmissionTest": 0.1624, + "Tests\\FrontendTest": 1.2526, + "Tests\\Git\\GitEventTest": 0.5507, + "Tests\\Git\\GitProcessTest": 1.0536, + "Tests\\Git\\GitTest": 3.4378, + "Tests\\GraphQL\\AssetInterfaceTest": 0.0159, + "Tests\\GraphQL\\EntryInterfaceTest": 0.015, + "Tests\\GraphQL\\GlobalSetInterfaceTest": 0.0223, + "Tests\\GraphQL\\ManagerTest": 0.2083, + "Tests\\GraphQL\\NavPageInterfaceTest": 0.0127, + "Tests\\GraphQL\\PageInterfaceTest": 0.0291, + "Tests\\GraphQL\\TermInterfaceTest": 0.0142, + "Tests\\Http\\Middleware\\AddViewPathsTest": 0.0933, + "Tests\\Http\\Middleware\\DeleteTemporaryFileUploadsTest": 0.0141, + "Tests\\Http\\Middleware\\HandleInertiaRequestsTest": 0.0772, + "Tests\\Http\\Middleware\\SelectedSiteTest": 0.2116, + "Tests\\Http\\Middleware\\StartSessionTest": 0.0728, + "Tests\\Http\\Resources\\CP\\Assets\\AssetsFieldtypeAssetTest": 0.0357, + "Tests\\Http\\View\\Composers\\JavascriptComposerTest": 0.12, + "Tests\\Imaging\\GlideImageManipulatorTest": 0.804, + "Tests\\Imaging\\GlideRoutePrefixTest": 0.0191, + "Tests\\Imaging\\GlideTest": 0.136, + "Tests\\Imaging\\GlideUrlBuilderTest": 0.0872, + "Tests\\Imaging\\GuzzleAdapterTest": 0.072, + "Tests\\Imaging\\ImageGeneratorTest": 0.282, + "Tests\\Imaging\\ImageValidatorTest": 0.0297, + "Tests\\Imaging\\ManagerTest": 0.0576, + "Tests\\Imaging\\PresetGeneratorTest": 0.0292, + "Tests\\Imaging\\RemoteUrlValidatorTest": 0.15, + "Tests\\Imaging\\StaticUrlBuilderTest": 0.0317, + "Tests\\Jobs\\ReportThemeUsageTest": 0.1993, + "Tests\\Licensing\\AddonLicenseTest": 0.116, + "Tests\\Licensing\\LicenseManagerTest": 0.1371, + "Tests\\Licensing\\OutpostTest": 0.1632, + "Tests\\Licensing\\SiteLicenseTest": 0.1096, + "Tests\\Licensing\\StatamicLicenseTest": 0.0939, + "Tests\\Listeners\\UpdateAssetReferencesTest": 1.5628, + "Tests\\Listeners\\UpdateTermReferencesTest": 0.5476, + "Tests\\Macros\\CollectionMacrosTest": 0.021, + "Tests\\Markdown\\ManagerTest": 0.0581, + "Tests\\Markdown\\MarkdownTest": 0.1859, + "Tests\\Markdown\\ParserTest": 0.0811, + "Tests\\MiscTest": 0.0664, + "Tests\\Modifiers\\AddQueryParamTest": 0.0294, + "Tests\\Modifiers\\AddSlashesTest": 0.03, + "Tests\\Modifiers\\AddTest": 0.0201, + "Tests\\Modifiers\\AliasTest": 0.0389, + "Tests\\Modifiers\\AmbersandListTest": 0.0589, + "Tests\\Modifiers\\AntlersTest": 0.031, + "Tests\\Modifiers\\AsciiTest": 0.0101, + "Tests\\Modifiers\\AtTest": 0.0475, + "Tests\\Modifiers\\AttributeTest": 0.1793, + "Tests\\Modifiers\\BackgroundPositionTest": 0.0985, + "Tests\\Modifiers\\BackspaceTest": 0.063, + "Tests\\Modifiers\\BardHtmlTest": 0.05, + "Tests\\Modifiers\\BardItemsTest": 0.0414, + "Tests\\Modifiers\\BardTextTest": 0.0788, + "Tests\\Modifiers\\CDataTest": 0.0104, + "Tests\\Modifiers\\CamelizeTest": 0.0604, + "Tests\\Modifiers\\CeilTest": 0.097, + "Tests\\Modifiers\\ChunkTest": 0.049, + "Tests\\Modifiers\\ClassesTest": 0.0105, + "Tests\\Modifiers\\CollapseTest": 0.0199, + "Tests\\Modifiers\\CollapseWhitespaceTest": 0.0668, + "Tests\\Modifiers\\CompactTest": 0.0111, + "Tests\\Modifiers\\ConsoleLogTest": 0.0176, + "Tests\\Modifiers\\ContainsTest": 0.0788, + "Tests\\Modifiers\\CountTest": 0.0402, + "Tests\\Modifiers\\DashifyTest": 0.0481, + "Tests\\Modifiers\\DaysAgoTest": 0.0801, + "Tests\\Modifiers\\DecodeTest": 0.0106, + "Tests\\Modifiers\\DeslugifyTest": 0.0203, + "Tests\\Modifiers\\DlTest": 0.0271, + "Tests\\Modifiers\\DoesntOverlapTest": 0.0525, + "Tests\\Modifiers\\EmbedUrlTest": 0.0703, + "Tests\\Modifiers\\EntitiesTest": 0.0115, + "Tests\\Modifiers\\ExplodeTest": 0.028, + "Tests\\Modifiers\\ExtensionTest": 0.0415, + "Tests\\Modifiers\\FaviconTest": 0.0111, + "Tests\\Modifiers\\FilterEmptyTest": 0.0281, + "Tests\\Modifiers\\FirstTest": 0.0675, + "Tests\\Modifiers\\FlattenTest": 0.1059, + "Tests\\Modifiers\\FlipTest": 0.0106, + "Tests\\Modifiers\\FloorTest": 0.1013, + "Tests\\Modifiers\\FluentModifyTest": 0.1771, + "Tests\\Modifiers\\FormatTest": 0.021, + "Tests\\Modifiers\\FormatTimeTest": 0.0679, + "Tests\\Modifiers\\FormatTranslatedTest": 0.0205, + "Tests\\Modifiers\\FullUrlsTest": 0.0105, + "Tests\\Modifiers\\GetTest": 0.0663, + "Tests\\Modifiers\\GroupByTest": 0.133, + "Tests\\Modifiers\\HasLowerCaseTest": 0.0444, + "Tests\\Modifiers\\HasUpperCaseTest": 0.0445, + "Tests\\Modifiers\\HexToRgbTest": 0.0113, + "Tests\\Modifiers\\HoursAgoTest": 0.0814, + "Tests\\Modifiers\\InArrayTest": 0.0182, + "Tests\\Modifiers\\IsAlphaTest": 0.0554, + "Tests\\Modifiers\\IsAlphanumericTest": 0.0657, + "Tests\\Modifiers\\IsArrayTest": 0.0593, + "Tests\\Modifiers\\IsBlankTest": 0.0246, + "Tests\\Modifiers\\IsEmailTest": 0.0233, + "Tests\\Modifiers\\IsEmbeddableTest": 0.051, + "Tests\\Modifiers\\IsEmptyTest": 0.0118, + "Tests\\Modifiers\\IsFutureTest": 0.0702, + "Tests\\Modifiers\\IsIterableTest": 0.0469, + "Tests\\Modifiers\\IsJsonTest": 0.0418, + "Tests\\Modifiers\\IsLeapYearTest": 0.0219, + "Tests\\Modifiers\\IsLowercaseTest": 0.0606, + "Tests\\Modifiers\\IsNumberwangTest": 0.1392, + "Tests\\Modifiers\\IsNumericTest": 0.0307, + "Tests\\Modifiers\\IsPastTest": 0.0598, + "Tests\\Modifiers\\IsTodayTest": 0.0504, + "Tests\\Modifiers\\IsTomorrowTest": 0.0606, + "Tests\\Modifiers\\IsUppercaseTest": 0.0612, + "Tests\\Modifiers\\IsUrlTest": 0.0684, + "Tests\\Modifiers\\IsWeekdayTest": 0.0206, + "Tests\\Modifiers\\IsWeekendTest": 0.0215, + "Tests\\Modifiers\\IsYesterdayTest": 0.0626, + "Tests\\Modifiers\\IsoFormatTest": 0.0209, + "Tests\\Modifiers\\JoinTest": 0.0384, + "Tests\\Modifiers\\KebabTest": 0.0406, + "Tests\\Modifiers\\KeyByTest": 0.0281, + "Tests\\Modifiers\\KeysTest": 0.0202, + "Tests\\Modifiers\\LastTest": 0.0816, + "Tests\\Modifiers\\LcfirstTest": 0.0111, + "Tests\\Modifiers\\LengthTest": 0.0698, + "Tests\\Modifiers\\LimitTest": 0.0305, + "Tests\\Modifiers\\LowerTest": 0.0182, + "Tests\\Modifiers\\MacroTest": 0.0214, + "Tests\\Modifiers\\MarkTest": 0.0787, + "Tests\\Modifiers\\MarkdownTest": 0.0208, + "Tests\\Modifiers\\Md5Test": 0.0099, + "Tests\\Modifiers\\MinutesAgoTest": 0.0773, + "Tests\\Modifiers\\ModifierTest": 0.0179, + "Tests\\Modifiers\\ModifyDateTest": 0.0103, + "Tests\\Modifiers\\MonthsAgoTest": 0.0792, + "Tests\\Modifiers\\NeatifyTest": 0.0105, + "Tests\\Modifiers\\Nl2brTest": 0.0197, + "Tests\\Modifiers\\ObfuscateTest": 0.118, + "Tests\\Modifiers\\OffsetTest": 0.0206, + "Tests\\Modifiers\\OlTest": 0.0207, + "Tests\\Modifiers\\OptionListTest": 0.0395, + "Tests\\Modifiers\\OverlapsTest": 0.0493, + "Tests\\Modifiers\\ParseUrlTest": 0.0287, + "Tests\\Modifiers\\PartialTest": 0.0111, + "Tests\\Modifiers\\PathinfoTest": 0.02, + "Tests\\Modifiers\\PluckTest": 0.0893, + "Tests\\Modifiers\\PluralTest": 0.0503, + "Tests\\Modifiers\\RandomTest": 0.0312, + "Tests\\Modifiers\\RegexMarkTest": 0.0187, + "Tests\\Modifiers\\RelativeTest": 0.0213, + "Tests\\Modifiers\\RemoveQueryParamTest": 0.032, + "Tests\\Modifiers\\ResolveTest": 0.0459, + "Tests\\Modifiers\\ReverseTest": 0.0323, + "Tests\\Modifiers\\ScopeTest": 0.0723, + "Tests\\Modifiers\\SecondsAgoTest": 0.0641, + "Tests\\Modifiers\\SegmentTest": 0.0205, + "Tests\\Modifiers\\SelectTest": 0.0922, + "Tests\\Modifiers\\SentenceListTest": 0.0401, + "Tests\\Modifiers\\SetQueryParamTest": 0.1139, + "Tests\\Modifiers\\ShrugTest": 0.0115, + "Tests\\Modifiers\\ShuffleTest": 0.0414, + "Tests\\Modifiers\\SingularTest": 0.0323, + "Tests\\Modifiers\\SlugifyTest": 0.0105, + "Tests\\Modifiers\\SmartypantsTest": 0.0186, + "Tests\\Modifiers\\SnakeTest": 0.0205, + "Tests\\Modifiers\\SortTest": 0.0839, + "Tests\\Modifiers\\SpacelessTest": 0.0103, + "Tests\\Modifiers\\StrPadBothTest": 0.0301, + "Tests\\Modifiers\\StrPadLeftTest": 0.0385, + "Tests\\Modifiers\\StrPadRightTest": 0.0303, + "Tests\\Modifiers\\StrPadTest": 0.0599, + "Tests\\Modifiers\\SumTest": 0.1307, + "Tests\\Modifiers\\TimezoneTest": 0.0211, + "Tests\\Modifiers\\TitleTest": 0.0106, + "Tests\\Modifiers\\ToBoolTest": 0.0103, + "Tests\\Modifiers\\ToJsonTest": 0.2151, + "Tests\\Modifiers\\ToQsTest": 0.0117, + "Tests\\Modifiers\\TrackableEmbedUrlTest": 0.0306, + "Tests\\Modifiers\\UlTest": 0.0283, + "Tests\\Modifiers\\UniqueTest": 0.0106, + "Tests\\Modifiers\\ValuesTest": 0.0198, + "Tests\\Modifiers\\WeeksAgoTest": 0.0884, + "Tests\\Modifiers\\WhereInTest": 0.0278, + "Tests\\Modifiers\\WhereTest": 0.0446, + "Tests\\Modifiers\\WidontTest": 0.102, + "Tests\\Modifiers\\YearsAgoTest": 0.0591, + "Tests\\OAuth\\OAuthCallbackTest": 0.3096, + "Tests\\OAuth\\OAuthConnectInitiationTest": 0.1048, + "Tests\\OAuth\\OAuthDisconnectTest": 0.7, + "Tests\\OAuth\\OAuthLoginTest": 0.0619, + "Tests\\OAuth\\OAuthPageTest": 0.0733, + "Tests\\OAuth\\OAuthRedirectTest": 0.0212, + "Tests\\OAuth\\OAuthTagsTest": 0.1427, + "Tests\\OAuth\\ProviderTest": 0.2426, + "Tests\\PathsTest": 0.1409, + "Tests\\Permissions\\CorePermissionsTest": 0.1315, + "Tests\\Permissions\\GateTest": 0.1966, + "Tests\\Permissions\\PermissionTest": 0.1693, + "Tests\\Permissions\\PermissionsTest": 0.1426, + "Tests\\PhoneHomeTest": 1.5713, + "Tests\\Policies\\AssetContainerPolicyTest": 0.054, + "Tests\\Policies\\AssetFolderPolicyTest": 0.2857, + "Tests\\Policies\\AssetPolicyTest": 0.3656, + "Tests\\Policies\\CollectionPolicyTest": 0.1128, + "Tests\\Policies\\EntryPolicyTest": 0.2115, + "Tests\\Policies\\GlobalSetPolicyTest": 0.1185, + "Tests\\Policies\\GlobalSetVariablesPolicyTest": 0.088, + "Tests\\Policies\\LocalizedTermPolicyTest": 0.1264, + "Tests\\Policies\\NavPolicyTest": 0.162, + "Tests\\Policies\\NavTreePolicyTest": 0.0927, + "Tests\\Policies\\SitePolicyTest": 0.0335, + "Tests\\Policies\\TaxonomyPolicyTest": 0.1178, + "Tests\\Policies\\TermPolicyTest": 0.127, + "Tests\\Preferences\\DefaultPreferencesTest": 0.0936, + "Tests\\Preferences\\EndpointsTest": 0.0869, + "Tests\\Preferences\\HasPreferencesInPropertyTraitTest": 0.149, + "Tests\\Preferences\\PrecedenceTest": 0.1435, + "Tests\\Preferences\\PreferencesTest": 0.1373, + "Tests\\Query\\FakesQueriesTest": 0.073, + "Tests\\Query\\FieldtypeFilterTest": 0.4464, + "Tests\\Query\\OrderByTest": 0.2898, + "Tests\\Query\\OrderedQueryBuilderTest": 0.0957, + "Tests\\Query\\ResolveValueTest": 0.2872, + "Tests\\Query\\StatusQueryBuilderTest": 0.3095, + "Tests\\Revisions\\RepositoryTest": 0.082, + "Tests\\Routing\\ResolveRedirectTest": 0.1978, + "Tests\\Routing\\RouteBindingTest": 5.2355, + "Tests\\Routing\\RouterMixinTest": 0.0116, + "Tests\\Routing\\RoutesTest": 0.5753, + "Tests\\Routing\\UrlBuilderTest": 0.2233, + "Tests\\Rules\\ComposerPackageTest": 0.0217, + "Tests\\Rules\\EmailAvailableTest": 0.036, + "Tests\\Rules\\HandleTest": 0.0307, + "Tests\\Rules\\SlugTest": 0.0299, + "Tests\\Search\\AlgoliaIndexTest": 0.0873, + "Tests\\Search\\AlgoliaQueryTest": 0.0109, + "Tests\\Search\\CombIndexTest": 0.0843, + "Tests\\Search\\CombTest": 0.1305, + "Tests\\Search\\Commands\\UpdateTest": 0.0385, + "Tests\\Search\\IndexManagerTest": 0.0423, + "Tests\\Search\\InsertMultipleJobTest": 0.0898, + "Tests\\Search\\QueryBuilderTest": 0.451, + "Tests\\Search\\SearchTest": 0.0719, + "Tests\\Search\\SearchablesTest": 0.2746, + "Tests\\Search\\Searchables\\AssetsTest": 0.2757, + "Tests\\Search\\Searchables\\EntriesTest": 0.3921, + "Tests\\Search\\Searchables\\TermsTest": 0.3334, + "Tests\\Search\\Searchables\\UsersTest": 0.1474, + "Tests\\Search\\UpdateItemIndexesTest": 0.0469, + "Tests\\Sites\\SiteTest": 0.2604, + "Tests\\Sites\\SitesConfigTest": 0.466, + "Tests\\Sites\\SitesTest": 0.2115, + "Tests\\Stache\\AggregateStoreTest": 0.0111, + "Tests\\Stache\\BasicStoreTest": 0.071, + "Tests\\Stache\\ColdStacheUriTest": 0.0591, + "Tests\\Stache\\DuplicatesTest": 0.0893, + "Tests\\Stache\\FeatureTest": 0.6443, + "Tests\\Stache\\Repositories\\AssetContainerRepositoryTest": 0.0677, + "Tests\\Stache\\Repositories\\CollectionRepositoryTest": 0.073, + "Tests\\Stache\\Repositories\\CollectionTreeRepositoryTest": 0.0414, + "Tests\\Stache\\Repositories\\EntryRepositoryTest": 0.355, + "Tests\\Stache\\Repositories\\GlobalRepositoryTest": 0.0764, + "Tests\\Stache\\Repositories\\GlobalVariablesRepositoryTest": 0.111, + "Tests\\Stache\\Repositories\\NavTreeRepositoryTest": 0.0214, + "Tests\\Stache\\Repositories\\NavigationRepositoryTest": 0.0635, + "Tests\\Stache\\Repositories\\SubmissionRepositoryTest": 0.0279, + "Tests\\Stache\\Repositories\\TaxonomyRepositoryTest": 0.1079, + "Tests\\Stache\\Repositories\\UserRepositoryTest": 0.0222, + "Tests\\Stache\\ServiceProviderTest": 0.0215, + "Tests\\Stache\\StacheTest": 0.2464, + "Tests\\Stache\\StoreTest": 0.0529, + "Tests\\Stache\\Stores\\AssetContainersStoreTest": 0.0559, + "Tests\\Stache\\Stores\\CollectionTreeStoreTest": 0.0674, + "Tests\\Stache\\Stores\\CollectionsStoreTest": 0.0666, + "Tests\\Stache\\Stores\\EntriesStoreTest": 0.2673, + "Tests\\Stache\\Stores\\FormSubmissionStoreTest": 0.0501, + "Tests\\Stache\\Stores\\GlobalVariablesStoreTest": 0.0552, + "Tests\\Stache\\Stores\\GlobalsStoreTest": 0.0666, + "Tests\\Stache\\Stores\\KeysTest": 0.0908, + "Tests\\Stache\\Stores\\NavTreeStoreTest": 0.0651, + "Tests\\Stache\\Stores\\NavigationStoreTest": 0.057, + "Tests\\Stache\\Stores\\TaxonomiesStoreTest": 0.0765, + "Tests\\Stache\\Stores\\TermsStoreTest": 0.0129, + "Tests\\Stache\\Stores\\UsersStoreTest": 0.0429, + "Tests\\Stache\\TraverserTest": 0.0452, + "Tests\\Stache\\WorkerInfiniteLoopTest": 0.0243, + "Tests\\StarterKits\\ExportTest": 0.8527, + "Tests\\StarterKits\\HookTest": 0.0232, + "Tests\\StarterKits\\InitTest": 0.2013, + "Tests\\StarterKits\\InstallTest": 10.0403, + "Tests\\StarterKits\\RunPostInstallTest": 0.5051, + "Tests\\StatamicTest": 0.6659, + "Tests\\StaticCaching\\ApplicationCacherTest": 0.2376, + "Tests\\StaticCaching\\CacherTest": 0.2099, + "Tests\\StaticCaching\\DefaultInvalidatorTest": 0.3551, + "Tests\\StaticCaching\\DefaultUrlExcluderTest": 0.0477, + "Tests\\StaticCaching\\FileCacherTest": 0.3393, + "Tests\\StaticCaching\\FullMeasureStaticCachingTest": 0.1519, + "Tests\\StaticCaching\\HalfMeasureStaticCachingTest": 0.3953, + "Tests\\StaticCaching\\InvalidateTest": 0.0804, + "Tests\\StaticCaching\\ManagerTest": 0.0498, + "Tests\\StaticCaching\\NoCacheDatabaseSessionTest": 0.0461, + "Tests\\StaticCaching\\NoCacheSessionTest": 0.2612, + "Tests\\StaticCaching\\NocacheRouteTest": 0.0364, + "Tests\\StaticCaching\\NocacheTagsTest": 0.1406, + "Tests\\StaticCaching\\RecacheTokenTest": 0.4194, + "Tests\\StaticCaching\\SharedErrorsStaticCachingTest": 0.0126, + "Tests\\StaticCaching\\UrlExcluderTest": 0.0602, + "Tests\\StaticCaching\\WriterTest": 0.0242, + "Tests\\Support\\ArrTest": 0.0198, + "Tests\\Support\\ComparatorTest": 0.0443, + "Tests\\Support\\HookableTest": 0.0238, + "Tests\\Support\\HtmlTest": 0.1902, + "Tests\\Support\\StrTest": 3.2436, + "Tests\\Support\\SvgTest": 0.3142, + "Tests\\Tags\\AssetsTest": 0.4038, + "Tests\\Tags\\CacheTagTest": 0.1878, + "Tests\\Tags\\ChildrenTest": 0.1495, + "Tests\\Tags\\Collection\\CollectionTest": 0.6788, + "Tests\\Tags\\Collection\\EntriesTest": 0.9069, + "Tests\\Tags\\Concerns\\GetsPipedArrayValuesTest": 0.015, + "Tests\\Tags\\Concerns\\QueriesConditionsTest": 0.8918, + "Tests\\Tags\\Concerns\\RendersAttributesTest": 0.069, + "Tests\\Tags\\Concerns\\RendersFormsTest": 2.4081, + "Tests\\Tags\\ContextTest": 0.1776, + "Tests\\Tags\\CookieTagTest": 0.0348, + "Tests\\Tags\\Dictionary\\DictionaryItemTest": 0.0323, + "Tests\\Tags\\Dictionary\\DictionaryTagTest": 0.1449, + "Tests\\Tags\\FluentTagTest": 0.1517, + "Tests\\Tags\\Form\\FormCreateAlpineTest": 0.7792, + "Tests\\Tags\\Form\\FormCreateCustomDriverTest": 0.2135, + "Tests\\Tags\\Form\\FormCreateTest": 0.9915, + "Tests\\Tags\\Form\\FormErrorsTest": 0.0478, + "Tests\\Tags\\Form\\FormSubmissionsTest": 0.0341, + "Tests\\Tags\\Form\\FormUploadValidationTest": 0.0981, + "Tests\\Tags\\GetContentTagTest": 0.3254, + "Tests\\Tags\\GetErrorTest": 0.0961, + "Tests\\Tags\\GetErrorsTest": 0.1466, + "Tests\\Tags\\GetSiteTagTest": 0.0532, + "Tests\\Tags\\GlideTest": 0.0857, + "Tests\\Tags\\IncrementTest": 0.0706, + "Tests\\Tags\\InstalledTest": 0.0608, + "Tests\\Tags\\IterateTest": 0.0794, + "Tests\\Tags\\LinkTest": 0.1588, + "Tests\\Tags\\LoaderTest": 0.012, + "Tests\\Tags\\LocalesTagTest": 0.2852, + "Tests\\Tags\\MountUrlTagTest": 0.6021, + "Tests\\Tags\\ParametersTest": 0.1936, + "Tests\\Tags\\ParentTest": 0.0629, + "Tests\\Tags\\PartialTagsTest": 0.1567, + "Tests\\Tags\\PathTest": 0.3973, + "Tests\\Tags\\RangeTest": 0.024, + "Tests\\Tags\\RedirectTest": 0.0606, + "Tests\\Tags\\SearchTest": 0.0573, + "Tests\\Tags\\SessionTagTest": 0.0477, + "Tests\\Tags\\StructureTagTest": 0.4522, + "Tests\\Tags\\SvgTagTest": 0.1296, + "Tests\\Tags\\TagsTest": 0.011, + "Tests\\Tags\\ThemeTagsTest": 0.257, + "Tests\\Tags\\TransTagTest": 0.0842, + "Tests\\Tags\\UserGroupsTagTest": 0.0669, + "Tests\\Tags\\UserRolesTagTest": 0.0728, + "Tests\\Tags\\User\\DeletePasskeyFormTest": 0.2826, + "Tests\\Tags\\User\\DisableTwoFactorFormTest": 0.1916, + "Tests\\Tags\\User\\ElevatedSessionFormTest": 0.8549, + "Tests\\Tags\\User\\ForgotPasswordFormTest": 0.6891, + "Tests\\Tags\\User\\LoginFormTest": 2.3377, + "Tests\\Tags\\User\\LogoutTest": 0.218, + "Tests\\Tags\\User\\PasskeyFormTest": 0.3318, + "Tests\\Tags\\User\\PasskeysTest": 0.0774, + "Tests\\Tags\\User\\PasswordFormTest": 1.5076, + "Tests\\Tags\\User\\ProfileFormTest": 0.2789, + "Tests\\Tags\\User\\RegisterFormTest": 0.9665, + "Tests\\Tags\\User\\ResetPasswordFormTest": 0.505, + "Tests\\Tags\\User\\ResetTwoFactorRecoveryCodesFormTest": 0.1334, + "Tests\\Tags\\User\\TwoFactorChallengeFormTest": 0.2382, + "Tests\\Tags\\User\\TwoFactorEnableFormTest": 0.2008, + "Tests\\Tags\\User\\TwoFactorRecoveryCodesTagTest": 0.0548, + "Tests\\Tags\\User\\TwoFactorSetupFormTest": 0.3349, + "Tests\\Tags\\User\\UserTagsTest": 0.2959, + "Tests\\Tags\\UsersTagsTest": 0.0551, + "Tests\\Tags\\ViteTest": 0.0661, + "Tests\\Tokens\\HandleTokenMiddlewareTest": 0.0883, + "Tests\\Tokens\\TokenRepositoryTest": 0.2159, + "Tests\\Tokens\\TokenTest": 0.1231, + "Tests\\Translator\\MethodDiscoveryTest": 0.0032, + "Tests\\Translator\\PlaceholdersTest": 0.0006, + "Tests\\TransposeCollectionMacroTest": 0.0112, + "Tests\\UpdateScripts\\AddPerEntryPermissionsTest": 0.0268, + "Tests\\UpdateScripts\\AddSitePermissionsTest": 0.0295, + "Tests\\UpdateScripts\\AddTimezoneConfigOptionsTest": 0.0231, + "Tests\\UpdateScripts\\MigrateSitesConfigToYamlTest": 0.1589, + "Tests\\UpdateScripts\\RemoveParentFieldTest": 0.0412, + "Tests\\UpdateScripts\\UpdateGlobalVariablesTest": 0.0991, + "Tests\\UpdateScripts\\UpdateScriptTest": 0.1962, + "Tests\\UpdateScripts\\UseClassBasedStatamicUniqueRulesTest": 0.0796, + "Tests\\Updater\\UpdatesOverviewTest": 0.1004, + "Tests\\Validation\\UniqueEntryValueTest": 0.0805, + "Tests\\Validation\\UniqueTermValueTest": 0.0798, + "Tests\\Validation\\UniqueUserValueTest": 0.0428, + "Tests\\View\\Antlers\\BladeLayoutTest": 0.0192, + "Tests\\View\\Antlers\\RuntimeParserEngineTest": 0.0566, + "Tests\\View\\Antlers\\ViewTest": 0.2207, + "Tests\\View\\Blade\\AntlersComponents\\ComponentCompilerTest": 0.426, + "Tests\\View\\Blade\\AntlersComponents\\NavCompilerTest": 0.1192, + "Tests\\View\\Blade\\AntlersComponents\\PartialCompilerTest": 0.2391, + "Tests\\View\\Blade\\AntlersComponents\\ReturnValuesTest": 0.1077, + "Tests\\View\\Blade\\AntlersComponents\\ScopeTagTest": 0.0129, + "Tests\\View\\Blade\\AntlersComponents\\SelfClosingTagsTest": 0.0159, + "Tests\\View\\Blade\\AntlersComponents\\TagContentsTest": 0.0152, + "Tests\\View\\Blade\\AntlersDirectiveTest": 0.0131, + "Tests\\View\\Blade\\CascadeDirectiveTest": 0.0667, + "Tests\\View\\Blade\\TagsDirectiveTest": 0.0861, + "Tests\\View\\CascadeTest": 0.6668, + "Tests\\View\\Scaffolding\\AntlersSourceEmitterTest": 0.5523, + "Tests\\View\\Scaffolding\\BladeSourceEmitterTest": 0.975, + "Tests\\View\\Scaffolding\\Fieldtypes\\ArrFieldtypeScaffoldingTest": 0.0589, + "Tests\\View\\Scaffolding\\Fieldtypes\\AssetsFieldtypeScaffoldingTest": 0.0923, + "Tests\\View\\Scaffolding\\Fieldtypes\\BardFieldtypeScaffoldingTest": 0.1143, + "Tests\\View\\Scaffolding\\Fieldtypes\\ButtonGroupFieldtypeScaffoldingTest": 0.0562, + "Tests\\View\\Scaffolding\\Fieldtypes\\CheckboxesFieldtypeScaffoldingTest": 0.0458, + "Tests\\View\\Scaffolding\\Fieldtypes\\CodeFieldtypeScaffoldingTest": 0.0556, + "Tests\\View\\Scaffolding\\Fieldtypes\\CollectionsFieldtypeScaffoldingTest": 0.1135, + "Tests\\View\\Scaffolding\\Fieldtypes\\ColorFieldtypeScaffoldingTest": 0.0556, + "Tests\\View\\Scaffolding\\Fieldtypes\\DateFieldtypeScaffoldingTest": 0.0997, + "Tests\\View\\Scaffolding\\Fieldtypes\\DictionaryFieldtypeScaffoldingTest": 0.2228, + "Tests\\View\\Scaffolding\\Fieldtypes\\EntriesFieldtypeScaffoldingTest": 0.0933, + "Tests\\View\\Scaffolding\\Fieldtypes\\FloatvalFieldtypeScaffoldingTest": 0.048, + "Tests\\View\\Scaffolding\\Fieldtypes\\FormFieldtypeScaffoldingTest": 0.1266, + "Tests\\View\\Scaffolding\\Fieldtypes\\GridFieldtypeScaffoldingTest": 0.0484, + "Tests\\View\\Scaffolding\\Fieldtypes\\GroupFieldtypeScaffoldingTest": 0.0342, + "Tests\\View\\Scaffolding\\Fieldtypes\\IconFieldtypeScaffoldingTest": 0.0448, + "Tests\\View\\Scaffolding\\Fieldtypes\\IntegerFieldtypeScaffoldingTest": 0.06, + "Tests\\View\\Scaffolding\\Fieldtypes\\LinkFieldtypeScaffoldingTest": 0.0446, + "Tests\\View\\Scaffolding\\Fieldtypes\\ListsFieldtypeScaffoldingTest": 0.0559, + "Tests\\View\\Scaffolding\\Fieldtypes\\MarkdownFieldtypeScaffoldingTest": 0.0569, + "Tests\\View\\Scaffolding\\Fieldtypes\\NavsFieldtypeScaffoldingTest": 0.1143, + "Tests\\View\\Scaffolding\\Fieldtypes\\RadioFieldtypeScaffoldingTest": 0.0465, + "Tests\\View\\Scaffolding\\Fieldtypes\\RangeFieldtypeScaffoldingTest": 0.0355, + "Tests\\View\\Scaffolding\\Fieldtypes\\ReplicatorFieldtypeScaffoldingTest": 0.1077, + "Tests\\View\\Scaffolding\\Fieldtypes\\SelectFieldtypeScaffoldingTest": 0.0537, + "Tests\\View\\Scaffolding\\Fieldtypes\\SitesFieldtypeScaffoldingTest": 0.1152, + "Tests\\View\\Scaffolding\\Fieldtypes\\SlugFieldtypeScaffoldingTest": 0.0456, + "Tests\\View\\Scaffolding\\Fieldtypes\\StructuresFieldtypeScaffoldingTest": 0.1431, + "Tests\\View\\Scaffolding\\Fieldtypes\\TableFieldtypeScaffoldingTest": 0.0463, + "Tests\\View\\Scaffolding\\Fieldtypes\\TaggableFieldtypeScaffoldingTest": 0.0547, + "Tests\\View\\Scaffolding\\Fieldtypes\\TaxonomiesFieldtypeScaffoldingTest": 0.1124, + "Tests\\View\\Scaffolding\\Fieldtypes\\TemplateFieldtypeScaffoldingTest": 0.0447, + "Tests\\View\\Scaffolding\\Fieldtypes\\TermsFieldtypeScaffoldingTest": 0.0458, + "Tests\\View\\Scaffolding\\Fieldtypes\\TextFieldtypeScaffoldingTest": 0.0583, + "Tests\\View\\Scaffolding\\Fieldtypes\\TextareaFieldtypeScaffoldingTest": 0.0451, + "Tests\\View\\Scaffolding\\Fieldtypes\\TimeFieldtypeScaffoldingTest": 0.0564, + "Tests\\View\\Scaffolding\\Fieldtypes\\ToggleFieldtypeScaffoldingTest": 0.0602, + "Tests\\View\\Scaffolding\\Fieldtypes\\UserGroupsFieldtypeScaffoldingTest": 0.1207, + "Tests\\View\\Scaffolding\\Fieldtypes\\UserRolesFieldtypeScaffoldingTest": 0.1148, + "Tests\\View\\Scaffolding\\Fieldtypes\\UsersFieldtypeScaffoldingTest": 0.2016, + "Tests\\View\\Scaffolding\\Fieldtypes\\VideoFieldtypeScaffoldingTest": 0.0565, + "Tests\\View\\Scaffolding\\Fieldtypes\\WidthFieldtypeScaffoldingTest": 0.0537, + "Tests\\View\\Scaffolding\\Fieldtypes\\YamlFieldtypeScaffoldingTest": 0.0213, + "Tests\\View\\Scaffolding\\SourceEmitterTest": 0.3673, + "Tests\\View\\Scaffolding\\TemplateGeneratorTest": 0.2869, + "Tests\\View\\StateTest": 0.0343, + "Tests\\Widgets\\WidgetTest": 0.0126, + "Tests\\Yaml\\YamlTest": 0.3288 + }, + "checksum": "bb2703bf880e415660c58cb17e33124b", + "updated_at": "2026-08-08T04:32:02+00:00" +} From 597341a9e6a7e9e526884a73b08076ce3e96a8d7 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:03:58 -0400 Subject: [PATCH 05/20] Return the nav item when creating one without a display name NavItem::display() is a fluent get-or-set, so passing null meant "get". Chaining create() off it returned the current display (null) instead of the item, and pushed that null into the registered items. Co-Authored-By: Claude Opus 5 (1M context) --- src/CP/Navigation/Nav.php | 4 +++- tests/CP/Navigation/NavTest.php | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/CP/Navigation/Nav.php b/src/CP/Navigation/Nav.php index 73b69d9c66..b8e342debe 100644 --- a/src/CP/Navigation/Nav.php +++ b/src/CP/Navigation/Nav.php @@ -26,7 +26,9 @@ public function extend(Closure $callback) */ public function create($name) { - $item = (new NavItem)->display($name); + $item = new NavItem; + + $item->display($name); $this->items[] = $item; diff --git a/tests/CP/Navigation/NavTest.php b/tests/CP/Navigation/NavTest.php index 29ce26e972..9c8cced489 100644 --- a/tests/CP/Navigation/NavTest.php +++ b/tests/CP/Navigation/NavTest.php @@ -69,6 +69,26 @@ public function it_can_more_explicitly_create_a_nav_item() $this->assertEquals('http://localhost/r2', $item->url()); } + #[Test] + public function it_returns_the_nav_item_when_created_without_a_display_name() + { + $item = Nav::create(null); + + $this->assertInstanceOf(NavItem::class, $item); + $this->assertNull($item->display()); + $this->assertEquals([$item], Nav::items()); + } + + #[Test] + public function it_returns_the_nav_item_when_created_without_a_display_name_using_the_item_alias() + { + $item = Nav::item(null); + + $this->assertInstanceOf(NavItem::class, $item); + $this->assertNull($item->display()); + $this->assertEquals([$item], Nav::items()); + } + #[Test] public function it_can_create_a_nav_item_with_a_more_custom_config() { From c314c34b0bffe02b9f771506b9e1b834adcd36cd Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:04:02 -0400 Subject: [PATCH 06/20] Fall back to the handle when a form has no title Matches collections, taxonomies, asset containers, globals, roles, and user groups, which all humanize the handle when no title is set. Without it, a title-less form gave a null nav item display and broke the CP nav. Co-Authored-By: Claude Opus 5 (1M context) --- src/Forms/Form.php | 7 ++++++- tests/CP/Navigation/CoreNavTest.php | 12 ++++++++++++ tests/Forms/FormTest.php | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Forms/Form.php b/src/Forms/Form.php index f55244d94c..c8833c92c3 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -75,7 +75,12 @@ public function handle($handle = null) */ public function title($title = null) { - return $this->fluentlyGetOrSet('title')->args(func_get_args()); + return $this + ->fluentlyGetOrSet('title') + ->getter(function ($title) { + return $title ?? ucfirst($this->handle); + }) + ->args(func_get_args()); } /** diff --git a/tests/CP/Navigation/CoreNavTest.php b/tests/CP/Navigation/CoreNavTest.php index 804b4ba86f..46f4cbaf4b 100644 --- a/tests/CP/Navigation/CoreNavTest.php +++ b/tests/CP/Navigation/CoreNavTest.php @@ -221,6 +221,18 @@ public function it_doesnt_build_globals_children_from_sites_that_the_user_is_not $this->assertEqualsCanonicalizing($expected, $actual); } + #[Test] + public function it_builds_the_nav_when_a_form_has_no_title() + { + Facades\Form::make('contact_us')->save(); + + $this->actingAs(tap(User::make()->makeSuper())->save()); + + $forms = $this->build()->get('Tools')->keyBy->display()->get('Forms'); + + $this->assertEquals(['Contact_us'], $forms->children()->map->display()->all()); + } + protected function build() { return Nav::build()->pluck('items', 'display'); diff --git a/tests/Forms/FormTest.php b/tests/Forms/FormTest.php index 8401c3adb6..fe31e2da64 100644 --- a/tests/Forms/FormTest.php +++ b/tests/Forms/FormTest.php @@ -11,6 +11,7 @@ use Statamic\Events\FormDeleting; use Statamic\Events\FormSaved; use Statamic\Events\FormSaving; +use Statamic\Facades\File; use Statamic\Facades\Form; use Statamic\Fields\Blueprint; use Tests\TestCase; @@ -24,6 +25,28 @@ public function setUp(): void Form::all()->each->delete(); } + #[Test] + public function it_falls_back_to_the_handle_for_the_title() + { + $form = Form::make('contact_us'); + + $this->assertEquals('Contact_us', $form->title()); + + $form->title('Contact Us'); + + $this->assertEquals('Contact Us', $form->title()); + } + + #[Test] + public function it_doesnt_save_the_fallback_title() + { + $form = Form::make('contact_us'); + + $form->save(); + + $this->assertStringNotContainsString('title', File::get($form->path())); + } + #[Test] public function it_saves_a_form() { From 5fcbb3e6c0fa44febad0ca075c67560c1916df01 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:21:02 -0400 Subject: [PATCH 07/20] Restore the testbench skeleton between tests The skeleton at vendor/orchestra/testbench-core/laravel persists for the life of a process, so anything a test writes there is visible to every test that follows it. 198 of our test files leave files behind, which is how a form file containing only {} ends up crashing CoreNavTest - it only passes today because of lucky ordering, and that luck runs out as soon as the suite is split across processes. Snapshot the skeleton once per process and delete anything new after each test. Directories the framework owns (bootstrap/cache, storage/framework/views and friends) are left alone, both because deleting them breaks the app and because walking them gets expensive. A process that starts against an already dirty skeleton would bake that dirt into its snapshot, so testbench.yaml declares the paths our tests are known to write and those get cleared before the first boot - which also means vendor/bin/testbench package:purge-skeleton cleans up after us. --- testbench.yaml | 44 ++++++++++ tests/RestoresTestbenchSkeleton.php | 126 ++++++++++++++++++++++++++++ tests/TestCase.php | 8 +- 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 testbench.yaml create mode 100644 tests/RestoresTestbenchSkeleton.php diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 0000000000..126288f437 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,44 @@ +# Everything the test suite is known to write into the testbench skeleton +# (vendor/orchestra/testbench-core/laravel). Tests/TestCase clears these once per +# process before snapshotting the skeleton, so a suite run never inherits leftovers +# from a previous one. It's also what `vendor/bin/testbench package:purge-skeleton` +# removes. +purge: + directories: + - addons + - app/Actions + - app/Dictionaries + - app/Fieldtypes + - app/Modifiers + - app/Scopes + - app/Tags + - app/Widgets + - config/statamic + - public/diskimgroot + - public/glide + - public/imgcache + - public/static + - public/testimages + - public/vendor + - resources/addons + - resources/blueprints + - resources/content + - resources/css + - resources/dictionaries + - resources/fieldsets + - resources/forms + - resources/js + - resources/users + - storage/framework/testing/disks + - storage/statamic + files: + - app/Providers/AppServiceProvider.php + - composer.json.bak + - composer.lock + - package.json + - public/*.jpg + - resources/*.svg + - resources/preferences.yaml + - resources/sites.yaml + - storage/logs/*.log + - vite-cp.config.js diff --git a/tests/RestoresTestbenchSkeleton.php b/tests/RestoresTestbenchSkeleton.php new file mode 100644 index 0000000000..4d4c852323 --- /dev/null +++ b/tests/RestoresTestbenchSkeleton.php @@ -0,0 +1,126 @@ +getPurgeAttributes(); + + $expand = fn ($paths) => (new Collection($paths)) + ->map(fn ($path) => default_skeleton_path().'/'.$path) + ->flatMap(fn ($path) => str_contains($path, '*') ? $files->glob($path) : [$path]); + + foreach ($expand($purge['files']) as $file) { + $files->delete($file); + } + + foreach ($expand($purge['directories']) as $directory) { + $files->deleteDirectory($directory); + } + } + + protected function snapshotTestbenchSkeleton(): void + { + if (self::$skeletonSnapshot !== null) { + return; + } + + self::$skeletonPath = $this->app->basePath(); + self::$skeletonSnapshot = $this->scanTestbenchSkeleton(); + } + + protected function restoreTestbenchSkeleton(): void + { + if (self::$skeletonSnapshot === null) { + return; + } + + $added = array_diff_key($this->scanTestbenchSkeleton(), self::$skeletonSnapshot); + + // Deepest first, so directories are empty by the time we get to them. + uksort($added, fn ($a, $b) => substr_count($b, '/') <=> substr_count($a, '/')); + + foreach ($added as $path => $isDir) { + $absolute = self::$skeletonPath.'/'.$path; + + $isDir ? @rmdir($absolute) : @unlink($absolute); + } + } + + private function scanTestbenchSkeleton(): array + { + $paths = []; + + $scan = function ($relative) use (&$scan, &$paths) { + $absolute = self::$skeletonPath.($relative ? '/'.$relative : ''); + + foreach (scandir($absolute) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $path = $relative ? $relative.'/'.$entry : $entry; + + if (in_array($path, self::$skeletonExclusions)) { + continue; + } + + $isDir = is_dir($absolute.'/'.$entry) && ! is_link($absolute.'/'.$entry); + + if ($isDir) { + $scan($path); + } + + $paths[$path] = $isDir; + } + }; + + $scan(''); + + return $paths; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index cc9f0f401a..95ea9a3bb2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,7 +12,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { - use WindowsHelpers; + use RestoresTestbenchSkeleton, WindowsHelpers; protected $shouldFakeVersion = true; protected $shouldPreventNavBeingBuilt = true; @@ -20,8 +20,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase protected function setUp(): void { + $this->purgeTestbenchSkeleton(); + parent::setUp(); + $this->snapshotTestbenchSkeleton(); + $this->withoutVite(); $this->withoutMiddleware(AuthenticateSession::class); @@ -58,6 +62,8 @@ public function tearDown(): void } parent::tearDown(); + + $this->restoreTestbenchSkeleton(); } protected function getPackageProviders($app) From d67d8420a5ffd617229e90cbf0f27a13c1726621 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:21:07 -0400 Subject: [PATCH 08/20] Stop MakeAddonTest running npm install against this repo Making an addon with a fieldtype runs 'npm install' from the testbench app's base path. That app has no package.json, so npm walks up and installs against ours, rewriting package-lock.json in the working tree. The other commands that trigger this already fake the process. --- tests/Console/Commands/MakeAddonTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Console/Commands/MakeAddonTest.php b/tests/Console/Commands/MakeAddonTest.php index d2e195cbfe..90c551f3f8 100644 --- a/tests/Console/Commands/MakeAddonTest.php +++ b/tests/Console/Commands/MakeAddonTest.php @@ -3,6 +3,7 @@ namespace Tests\Console\Commands; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\Process; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -19,6 +20,10 @@ public function setUp(): void $this->markTestSkippedInWindows(); + // Without this, the addon's `npm install` runs for real. Since the testbench app + // has no package.json, npm walks up and installs against this repo's own one. + Process::fake(); + $this->files = app(Filesystem::class); $this->fakeSuccessfulComposerRequire(); } From a72607ab21268cfbe0f8fecb0c7522e7c838a879 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:21:07 -0400 Subject: [PATCH 09/20] Stop DuplicateFormTest writing users into the fixtures directory The users it makes have no id, so saving them writes tests/__fixtures__/users/.yaml into the repo. Point the stache stores at the throwaway directory like the other tests that save users do. --- tests/Actions/DuplicateFormTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Actions/DuplicateFormTest.php b/tests/Actions/DuplicateFormTest.php index 2b44565a8f..4ce7839e21 100644 --- a/tests/Actions/DuplicateFormTest.php +++ b/tests/Actions/DuplicateFormTest.php @@ -7,11 +7,13 @@ use Statamic\Facades\Form; use Statamic\Facades\User; use Tests\FakesRoles; +use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; class DuplicateFormTest extends TestCase { use FakesRoles; + use PreventSavingStacheItemsToDisk; public function setUp(): void { From 2072e5762853b02d97406561aaf890e5bfeae53e Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:21:12 -0400 Subject: [PATCH 10/20] Create the blueprint ViewBlueprintListingTest needs It was asserting a custom namespace blueprint could be edited without ever creating one, and only passed because StoreCustomBlueprintTest had left one behind in the testbench skeleton. --- tests/Feature/Blueprints/ViewBlueprintListingTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Blueprints/ViewBlueprintListingTest.php b/tests/Feature/Blueprints/ViewBlueprintListingTest.php index 86feed329f..f1394c48ec 100644 --- a/tests/Feature/Blueprints/ViewBlueprintListingTest.php +++ b/tests/Feature/Blueprints/ViewBlueprintListingTest.php @@ -51,6 +51,8 @@ public function it_lets_you_edit_a_custom_namespace_blueprint() Facades\Blueprint::addNamespace($namespace, 'resources/content/'.$namespace); + $this->createBlueprint($namespace, $handle)->save(); + $this ->actingAs($user) ->get(cp_route('blueprints.additional.edit', [$namespace, $handle])) @@ -58,8 +60,8 @@ public function it_lets_you_edit_a_custom_namespace_blueprint() ->assertInertia(fn ($page) => $page->component('blueprints/Edit')); } - private function createBlueprint($handle) + private function createBlueprint($namespace, $handle) { - return tap(new Blueprint)->setHandle($handle); + return tap(new Blueprint)->setHandle($handle)->setNamespace($namespace); } } From c403c7dbccbe07235d049ea2dc9f47910da97532 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 10:21:12 -0400 Subject: [PATCH 11/20] Create glide's temp directory in the non-glideable upload test Glide only makes the directory when it actually processes an image, which by definition never happens here. The test was relying on an earlier one in the file having made it, so make it up front and keep the assertion that nothing lands in it. --- tests/Assets/AssetTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Assets/AssetTest.php b/tests/Assets/AssetTest.php index 516df9febd..de52a40a57 100644 --- a/tests/Assets/AssetTest.php +++ b/tests/Assets/AssetTest.php @@ -2112,6 +2112,10 @@ public function it_doesnt_process_or_error_when_uploading_non_glideable_file_wit $this->container->sourcePreset('small'); + // Glide only creates its temp directory when it actually processes an image, so + // create it up front. Otherwise there'd be nothing for the assertion below to check. + app('files')->makeDirectory($glideDir = storage_path('statamic/glide/tmp'), 0777, true, true); + $asset = (new Asset)->container($this->container)->path("path/to/file.{$extension}")->syncOriginal(); Facades\AssetContainer::shouldReceive('findByHandle')->with('test_container')->andReturn($this->container); @@ -2123,7 +2127,6 @@ public function it_doesnt_process_or_error_when_uploading_non_glideable_file_wit $return = $asset->upload(UploadedFile::fake()->createWithContent("file.{$extension}", '')); $this->assertEquals($asset, $return); - $this->assertDirectoryExists($glideDir = storage_path('statamic/glide/tmp')); $this->assertEmpty(app('files')->allFiles($glideDir)); // no temp files Storage::disk('test')->assertExists("path/to/file.{$extension}"); $this->assertEquals("path/to/file.{$extension}", $asset->path()); From 2a3051245b56a9377c2e3b54162d1b054ba85832 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 22:57:12 -0400 Subject: [PATCH 12/20] TEMP: disable fail-fast for validation run Temporary. So one shard failing doesn't cancel the rest of the matrix and we see every remaining failure in a single run. Revert before merge. --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3f96cbe602..cc7bce7c40 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,7 @@ jobs: contents: read strategy: + fail-fast: false matrix: php: [8.3, 8.4, 8.5] laravel: [12.*, 13.*] From 981142511702be28b1de5e17e841d2095bb0a2f4 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 23:50:20 -0400 Subject: [PATCH 13/20] Add throwaway Windows junction probe Not for merging. Confirms two things on Windows CI: 1. PHP does not recurse into mklink /J junctions via RecursiveDirectoryIterator with FOLLOW_SYMLINKS, but does recurse into real symlinks. 2. deleteDirectory() cannot remove directory symlinks, so TemplateFolderTest leaks them into the directory TemplatesTest reuses. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/windows-junction-probe.yml | 75 +++++++++++++++++ windows-junction-probe.php | 89 ++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 .github/workflows/windows-junction-probe.yml create mode 100644 windows-junction-probe.php diff --git a/.github/workflows/windows-junction-probe.yml b/.github/workflows/windows-junction-probe.yml new file mode 100644 index 0000000000..5609c8fd70 --- /dev/null +++ b/.github/workflows/windows-junction-probe.yml @@ -0,0 +1,75 @@ +name: Windows Junction Probe + +on: + push: + branches: + - 'probe/**' + +permissions: {} + +jobs: + probe: + runs-on: windows-latest + permissions: + contents: read + + name: Windows junction probe + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: 8.5 + extensions: fileinfo, exif, gd, pdo, sqlite, pdo_sqlite + ini-values: short_open_tag=on + coverage: none + + - name: Probe the filesystem behaviour + run: php windows-junction-probe.php + + - name: Install dependencies + run: | + composer require "illuminate/contracts:12.*" --dev --no-interaction --no-update + composer update --prefer-stable --prefer-dist --no-interaction + + - name: TemplatesTest in isolation (predicted FAIL) + id: isolated + continue-on-error: true + run: vendor/bin/phpunit tests/Fieldtypes/TemplatesTest.php + + - name: Leftovers after the isolated run + if: always() + shell: cmd + run: | + dir /AL /S tests\Fieldtypes\templates-test-tmp + exit /b 0 + + - name: Clean up between runs + if: always() + shell: cmd + run: | + rmdir /S /Q tests\Fieldtypes\templates-test-tmp + exit /b 0 + + - name: The whole Fieldtypes directory (predicted PASS) + id: together + continue-on-error: true + run: vendor/bin/phpunit tests/Fieldtypes + + - name: Leftovers after the combined run + if: always() + shell: cmd + run: | + dir /AL /S tests\Fieldtypes\templates-test-tmp + exit /b 0 + + - name: Summary + if: always() + run: | + Write-Output "TemplatesTest in isolation: ${{ steps.isolated.outcome }} (predicted failure)" + Write-Output "Whole Fieldtypes directory: ${{ steps.together.outcome }} (predicted success)" diff --git a/windows-junction-probe.php b/windows-junction-probe.php new file mode 100644 index 0000000000..dc9e0717c6 --- /dev/null +++ b/windows-junction-probe.php @@ -0,0 +1,89 @@ +getPathname()); +} + +sort($found); + +foreach ($found as $path) { + echo ' '.$path.PHP_EOL; +} + +echo PHP_EOL.'== 4. Can unlink() remove them? (this is what deleteDirectory() calls) =='.PHP_EOL; + +echo ' unlink(symlinked): '.var_export(@unlink($symlink), true).PHP_EOL; +clearstatcache(true, $symlink); +echo ' still present: '.var_export(file_exists($symlink) || is_link($symlink), true).PHP_EOL; + +echo ' unlink(junction): '.var_export(@unlink($junction), true).PHP_EOL; +clearstatcache(true, $junction); +echo ' still present: '.var_export(file_exists($junction) || is_link($junction), true).PHP_EOL; + +echo ' rmdir(junction): '.var_export(@rmdir($junction), true).PHP_EOL; +clearstatcache(true, $junction); +echo ' still present: '.var_export(file_exists($junction) || is_link($junction), true).PHP_EOL; + +echo PHP_EOL.'== Predictions =='.PHP_EOL; +echo ' mklink /J exits 0; is_dir(junction) is false but is_dir(symlinked) is true;'.PHP_EOL; +echo ' the iterator yields "junction" and "empty-junction" as leaves but descends'.PHP_EOL; +echo ' into "symlinked"; unlink() fails on both links, rmdir() removes the junction.'.PHP_EOL; From 3bd1ab2ff1b45ae7520636b157e3f6b964525b81 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sat, 8 Aug 2026 23:57:18 -0400 Subject: [PATCH 14/20] Probe a candidate fix for junction traversal Overrides hasChildren() to fall back to is_dir(), which is true for junctions. Checks that recursion continues from the unresolved pathname so template names keep their virtual prefix. Co-Authored-By: Claude Opus 5 (1M context) --- windows-junction-probe.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/windows-junction-probe.php b/windows-junction-probe.php index dc9e0717c6..43b1d84cd0 100644 --- a/windows-junction-probe.php +++ b/windows-junction-probe.php @@ -22,6 +22,17 @@ function probe(string $label, string $path): void ); } +// Candidate fix. hasChildren() takes the "not a link" branch for junctions and +// returns S_ISDIR() of an lstat mode that is neither, so it never reaches the +// FOLLOW_SYMLINKS branch. is_dir() is true for junctions, so fall back to it. +class ViewDirectoryIterator extends RecursiveDirectoryIterator +{ + public function hasChildren(bool $allowLinks = false): bool + { + return parent::hasChildren($allowLinks) || is_dir($this->getPathname()); + } +} + echo 'PHP '.PHP_VERSION.' on '.PHP_OS_FAMILY.PHP_EOL.PHP_EOL; @mkdir($base.'/views', 0777, true); @@ -69,6 +80,25 @@ function probe(string $label, string $path): void echo ' '.$path.PHP_EOL; } +echo PHP_EOL.'== 3b. Candidate fix: override hasChildren() to fall back to is_dir() =='.PHP_EOL; + +$fixed = []; + +foreach (new RecursiveIteratorIterator( + new ViewDirectoryIterator( + $base.'/views', + FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS + ) +) as $file) { + $fixed[] = str_replace($base.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR, '', $file->getPathname()); +} + +sort($fixed); + +foreach ($fixed as $path) { + echo ' '.$path.PHP_EOL; +} + echo PHP_EOL.'== 4. Can unlink() remove them? (this is what deleteDirectory() calls) =='.PHP_EOL; echo ' unlink(symlinked): '.var_export(@unlink($symlink), true).PHP_EOL; @@ -87,3 +117,6 @@ function probe(string $label, string $path): void echo ' mklink /J exits 0; is_dir(junction) is false but is_dir(symlinked) is true;'.PHP_EOL; echo ' the iterator yields "junction" and "empty-junction" as leaves but descends'.PHP_EOL; echo ' into "symlinked"; unlink() fails on both links, rmdir() removes the junction.'.PHP_EOL; +echo ' With the candidate fix, 3b descends into the junction too, yielding'.PHP_EOL; +echo ' "junction\tango.html" (virtual path preserved, not resolved to "target\..."),'.PHP_EOL; +echo ' and "empty-junction" disappears entirely rather than becoming a leaf.'.PHP_EOL; From bc499f7cf06c2463cedf8e61b1752e3aa990149e Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sun, 9 Aug 2026 00:00:15 -0400 Subject: [PATCH 15/20] Instrument the hasChildren override and probe a hand-rolled scan The override had no effect, so log whether it is invoked at all, and try a plain recursive scan that leans on is_dir() instead of SPL recursion. Co-Authored-By: Claude Opus 5 (1M context) --- windows-junction-probe.php | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/windows-junction-probe.php b/windows-junction-probe.php index 43b1d84cd0..d373c5cb7d 100644 --- a/windows-junction-probe.php +++ b/windows-junction-probe.php @@ -29,7 +29,14 @@ class ViewDirectoryIterator extends RecursiveDirectoryIterator { public function hasChildren(bool $allowLinks = false): bool { - return parent::hasChildren($allowLinks) || is_dir($this->getPathname()); + $parent = parent::hasChildren($allowLinks); + $isDir = is_dir($this->getPathname()); + + echo ' [hasChildren] '.$this->getPathname() + .' parent='.var_export($parent, true) + .' is_dir='.var_export($isDir, true).PHP_EOL; + + return $parent || $isDir; } } @@ -99,6 +106,38 @@ public function hasChildren(bool $allowLinks = false): bool echo ' '.$path.PHP_EOL; } +echo PHP_EOL.'== 3c. Alternative: hand-rolled recursion using is_dir() =='.PHP_EOL; + +function scan(string $dir, string $prefix = ''): array +{ + $found = []; + + foreach (new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS) as $file) { + $name = $file->getFilename(); + + if (str_starts_with($name, '.') || $name === 'node_modules') { + continue; + } + + $path = $dir.DIRECTORY_SEPARATOR.$name; + + if (is_dir($path)) { + $found = array_merge($found, scan($path, $prefix.$name.DIRECTORY_SEPARATOR)); + } else { + $found[] = $prefix.$name; + } + } + + return $found; +} + +$handRolled = scan($base.'/views'); +sort($handRolled); + +foreach ($handRolled as $path) { + echo ' '.$path.PHP_EOL; +} + echo PHP_EOL.'== 4. Can unlink() remove them? (this is what deleteDirectory() calls) =='.PHP_EOL; echo ' unlink(symlinked): '.var_export(@unlink($symlink), true).PHP_EOL; From 986375620e7177622a62f20d923f48ff1b146867 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sun, 9 Aug 2026 00:02:53 -0400 Subject: [PATCH 16/20] Test whether clearstatcache rescues the hasChildren override parent::hasChildren() lstat()s the path first, and is_dir() appears to reuse that cached result. Compare is_dir() before and after clearing the cache. Co-Authored-By: Claude Opus 5 (1M context) --- windows-junction-probe.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/windows-junction-probe.php b/windows-junction-probe.php index d373c5cb7d..93854c7b1b 100644 --- a/windows-junction-probe.php +++ b/windows-junction-probe.php @@ -29,14 +29,22 @@ class ViewDirectoryIterator extends RecursiveDirectoryIterator { public function hasChildren(bool $allowLinks = false): bool { + $path = $this->getPathname(); + $parent = parent::hasChildren($allowLinks); - $isDir = is_dir($this->getPathname()); - echo ' [hasChildren] '.$this->getPathname() + // parent::hasChildren() lstat()s the path, and the cached lstat result is + // what is_dir() then sees. Drop it so is_dir() does a real stat(). + $stale = is_dir($path); + clearstatcache(true, $path); + $fresh = is_dir($path); + + echo ' [hasChildren] '.$path .' parent='.var_export($parent, true) - .' is_dir='.var_export($isDir, true).PHP_EOL; + .' is_dir(stale)='.var_export($stale, true) + .' is_dir(fresh)='.var_export($fresh, true).PHP_EOL; - return $parent || $isDir; + return $parent || $fresh; } } From 9951e14d1dae4c65ae1189ce0809829651be7091 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sun, 9 Aug 2026 00:11:05 -0400 Subject: [PATCH 17/20] Remove the probe Its findings are applied in the commits that follow. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/windows-junction-probe.yml | 75 -------- windows-junction-probe.php | 169 ------------------- 2 files changed, 244 deletions(-) delete mode 100644 .github/workflows/windows-junction-probe.yml delete mode 100644 windows-junction-probe.php diff --git a/.github/workflows/windows-junction-probe.yml b/.github/workflows/windows-junction-probe.yml deleted file mode 100644 index 5609c8fd70..0000000000 --- a/.github/workflows/windows-junction-probe.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Windows Junction Probe - -on: - push: - branches: - - 'probe/**' - -permissions: {} - -jobs: - probe: - runs-on: windows-latest - permissions: - contents: read - - name: Windows junction probe - - steps: - - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup PHP - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 - with: - php-version: 8.5 - extensions: fileinfo, exif, gd, pdo, sqlite, pdo_sqlite - ini-values: short_open_tag=on - coverage: none - - - name: Probe the filesystem behaviour - run: php windows-junction-probe.php - - - name: Install dependencies - run: | - composer require "illuminate/contracts:12.*" --dev --no-interaction --no-update - composer update --prefer-stable --prefer-dist --no-interaction - - - name: TemplatesTest in isolation (predicted FAIL) - id: isolated - continue-on-error: true - run: vendor/bin/phpunit tests/Fieldtypes/TemplatesTest.php - - - name: Leftovers after the isolated run - if: always() - shell: cmd - run: | - dir /AL /S tests\Fieldtypes\templates-test-tmp - exit /b 0 - - - name: Clean up between runs - if: always() - shell: cmd - run: | - rmdir /S /Q tests\Fieldtypes\templates-test-tmp - exit /b 0 - - - name: The whole Fieldtypes directory (predicted PASS) - id: together - continue-on-error: true - run: vendor/bin/phpunit tests/Fieldtypes - - - name: Leftovers after the combined run - if: always() - shell: cmd - run: | - dir /AL /S tests\Fieldtypes\templates-test-tmp - exit /b 0 - - - name: Summary - if: always() - run: | - Write-Output "TemplatesTest in isolation: ${{ steps.isolated.outcome }} (predicted failure)" - Write-Output "Whole Fieldtypes directory: ${{ steps.together.outcome }} (predicted success)" diff --git a/windows-junction-probe.php b/windows-junction-probe.php deleted file mode 100644 index 93854c7b1b..0000000000 --- a/windows-junction-probe.php +++ /dev/null @@ -1,169 +0,0 @@ -getPathname(); - - $parent = parent::hasChildren($allowLinks); - - // parent::hasChildren() lstat()s the path, and the cached lstat result is - // what is_dir() then sees. Drop it so is_dir() does a real stat(). - $stale = is_dir($path); - clearstatcache(true, $path); - $fresh = is_dir($path); - - echo ' [hasChildren] '.$path - .' parent='.var_export($parent, true) - .' is_dir(stale)='.var_export($stale, true) - .' is_dir(fresh)='.var_export($fresh, true).PHP_EOL; - - return $parent || $fresh; - } -} - -echo 'PHP '.PHP_VERSION.' on '.PHP_OS_FAMILY.PHP_EOL.PHP_EOL; - -@mkdir($base.'/views', 0777, true); -@mkdir($base.'/target/three', 0777, true); -@mkdir($base.'/empty-target', 0777, true); -file_put_contents($base.'/target/tango.html', ''); -file_put_contents($base.'/target/three/uniform.html', ''); - -echo '== 1. Creating the links =='.PHP_EOL; - -$junction = $base.'/views/junction'; -exec('mklink /J '.escapeshellarg($junction).' '.escapeshellarg($base.'/target'), $output, $exit); -echo ' mklink /J exit code: '.$exit.PHP_EOL; -echo ' mklink /J output: '.implode(' | ', $output).PHP_EOL; - -$symlink = $base.'/views/symlinked'; -echo ' symlink() returned: '.var_export(@symlink($base.'/target', $symlink), true).PHP_EOL; - -$emptyJunction = $base.'/views/empty-junction'; -exec('mklink /J '.escapeshellarg($emptyJunction).' '.escapeshellarg($base.'/empty-target'), $output2, $exit2); -echo ' mklink /J (empty) exit code: '.$exit2.PHP_EOL; - -echo PHP_EOL.'== 2. How PHP sees them =='.PHP_EOL; -probe('junction', $junction); -probe('symlinked', $symlink); -probe('real dir', $base.'/target'); - -echo PHP_EOL.'== 3. RecursiveDirectoryIterator, SKIP_DOTS|FOLLOW_SYMLINKS, LEAVES_ONLY =='.PHP_EOL; -echo ' (this is exactly what TemplatesController::index() does)'.PHP_EOL; - -$found = []; - -foreach (new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( - $base.'/views', - FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS - ) -) as $file) { - $found[] = str_replace($base.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR, '', $file->getPathname()); -} - -sort($found); - -foreach ($found as $path) { - echo ' '.$path.PHP_EOL; -} - -echo PHP_EOL.'== 3b. Candidate fix: override hasChildren() to fall back to is_dir() =='.PHP_EOL; - -$fixed = []; - -foreach (new RecursiveIteratorIterator( - new ViewDirectoryIterator( - $base.'/views', - FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS - ) -) as $file) { - $fixed[] = str_replace($base.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR, '', $file->getPathname()); -} - -sort($fixed); - -foreach ($fixed as $path) { - echo ' '.$path.PHP_EOL; -} - -echo PHP_EOL.'== 3c. Alternative: hand-rolled recursion using is_dir() =='.PHP_EOL; - -function scan(string $dir, string $prefix = ''): array -{ - $found = []; - - foreach (new FilesystemIterator($dir, FilesystemIterator::SKIP_DOTS) as $file) { - $name = $file->getFilename(); - - if (str_starts_with($name, '.') || $name === 'node_modules') { - continue; - } - - $path = $dir.DIRECTORY_SEPARATOR.$name; - - if (is_dir($path)) { - $found = array_merge($found, scan($path, $prefix.$name.DIRECTORY_SEPARATOR)); - } else { - $found[] = $prefix.$name; - } - } - - return $found; -} - -$handRolled = scan($base.'/views'); -sort($handRolled); - -foreach ($handRolled as $path) { - echo ' '.$path.PHP_EOL; -} - -echo PHP_EOL.'== 4. Can unlink() remove them? (this is what deleteDirectory() calls) =='.PHP_EOL; - -echo ' unlink(symlinked): '.var_export(@unlink($symlink), true).PHP_EOL; -clearstatcache(true, $symlink); -echo ' still present: '.var_export(file_exists($symlink) || is_link($symlink), true).PHP_EOL; - -echo ' unlink(junction): '.var_export(@unlink($junction), true).PHP_EOL; -clearstatcache(true, $junction); -echo ' still present: '.var_export(file_exists($junction) || is_link($junction), true).PHP_EOL; - -echo ' rmdir(junction): '.var_export(@rmdir($junction), true).PHP_EOL; -clearstatcache(true, $junction); -echo ' still present: '.var_export(file_exists($junction) || is_link($junction), true).PHP_EOL; - -echo PHP_EOL.'== Predictions =='.PHP_EOL; -echo ' mklink /J exits 0; is_dir(junction) is false but is_dir(symlinked) is true;'.PHP_EOL; -echo ' the iterator yields "junction" and "empty-junction" as leaves but descends'.PHP_EOL; -echo ' into "symlinked"; unlink() fails on both links, rmdir() removes the junction.'.PHP_EOL; -echo ' With the candidate fix, 3b descends into the junction too, yielding'.PHP_EOL; -echo ' "junction\tango.html" (virtual path preserved, not resolved to "target\..."),'.PHP_EOL; -echo ' and "empty-junction" disappears entirely rather than becoming a leaf.'.PHP_EOL; From 334ebaf86d8e5580fa58113678f9abe5c32f472f Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sun, 9 Aug 2026 00:12:42 -0400 Subject: [PATCH 18/20] Stop the template fieldtype tests leaking into each other Both classes shared tests/Fieldtypes/templates-test-tmp, and their teardown used deleteDirectory(), which cannot remove a symlinked or junctioned directory on Windows. TemplateFolderTest's symlinks therefore survived into TemplatesTest, which reused them instead of the links it thought it had created. Give each class its own directory and a teardown that can remove reparse points. Co-Authored-By: Claude Opus 5 (1M context) --- tests/DeletesDirectories.php | 43 +++++++++++++++++++++++++ tests/Fieldtypes/TemplateFolderTest.php | 7 ++-- tests/Fieldtypes/TemplatesTest.php | 5 +-- 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 tests/DeletesDirectories.php diff --git a/tests/DeletesDirectories.php b/tests/DeletesDirectories.php new file mode 100644 index 0000000000..14ccc727e9 --- /dev/null +++ b/tests/DeletesDirectories.php @@ -0,0 +1,43 @@ +getPathname(); + + if (is_link($path)) { + @unlink($path) || @rmdir($path); + + continue; + } + + if (! is_dir($path)) { + @unlink($path); + + continue; + } + + // A junction has neither a link nor a directory lstat mode, so it is + // indistinguishable from a directory here. rmdir() removes an empty + // directory or a junction, and leaves the junction's target alone. + if (! @rmdir($path)) { + $this->deleteDirectory($path); + } + } + + @rmdir($directory); + } +} diff --git a/tests/Fieldtypes/TemplateFolderTest.php b/tests/Fieldtypes/TemplateFolderTest.php index 6c8217c4df..be262ab570 100644 --- a/tests/Fieldtypes/TemplateFolderTest.php +++ b/tests/Fieldtypes/TemplateFolderTest.php @@ -6,24 +6,27 @@ use Statamic\Facades\File; use Statamic\Fields\Field; use Statamic\Fieldtypes\TemplateFolder; +use Tests\DeletesDirectories; use Tests\TestCase; class TemplateFolderTest extends TestCase { + use DeletesDirectories; + private string $dir; public function setUp(): void { parent::setUp(); - app('files')->makeDirectory($this->dir = __DIR__.'/templates-test-tmp', force: true); + app('files')->makeDirectory($this->dir = __DIR__.'/template-folder-test-tmp', force: true); $this->app['config']->set('view.paths', [$this->dir.'/views']); } public function tearDown(): void { - app('files')->deleteDirectory($this->dir); + $this->deleteDirectory($this->dir); parent::tearDown(); } diff --git a/tests/Fieldtypes/TemplatesTest.php b/tests/Fieldtypes/TemplatesTest.php index 01653f7e83..10c4145437 100644 --- a/tests/Fieldtypes/TemplatesTest.php +++ b/tests/Fieldtypes/TemplatesTest.php @@ -5,12 +5,13 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\File; use Statamic\Facades\User; +use Tests\DeletesDirectories; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; class TemplatesTest extends TestCase { - use PreventSavingStacheItemsToDisk; + use DeletesDirectories, PreventSavingStacheItemsToDisk; private string $dir; @@ -25,7 +26,7 @@ public function setUp(): void public function tearDown(): void { - app('files')->deleteDirectory($this->dir); + $this->deleteDirectory($this->dir); parent::tearDown(); } From f18f80059d8ce26649d3c5a9d15ede2b6007193e Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sun, 9 Aug 2026 00:12:46 -0400 Subject: [PATCH 19/20] Traverse Windows junctions when scanning view paths RecursiveDirectoryIterator treats a junction as a leaf, so a junctioned folder under a view path was offered as a bogus template and the templates inside it were invisible. Both the Templates and Template Folder fieldtypes were affected. Co-Authored-By: Claude Opus 5 (1M context) --- src/Fieldtypes/TemplateFolder.php | 2 +- src/Filesystem/RecursiveDirectoryIterator.php | 25 +++++++++++++++++++ .../CP/API/TemplatesController.php | 2 +- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/Filesystem/RecursiveDirectoryIterator.php diff --git a/src/Fieldtypes/TemplateFolder.php b/src/Fieldtypes/TemplateFolder.php index e59144ec55..2a2d361f58 100644 --- a/src/Fieldtypes/TemplateFolder.php +++ b/src/Fieldtypes/TemplateFolder.php @@ -4,8 +4,8 @@ use FilesystemIterator; use RecursiveCallbackFilterIterator; -use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use Statamic\Filesystem\RecursiveDirectoryIterator; use Statamic\Support\Str; class TemplateFolder extends Relationship diff --git a/src/Filesystem/RecursiveDirectoryIterator.php b/src/Filesystem/RecursiveDirectoryIterator.php new file mode 100644 index 0000000000..49ccdf30bb --- /dev/null +++ b/src/Filesystem/RecursiveDirectoryIterator.php @@ -0,0 +1,25 @@ +getPathname()); + + return is_dir($path); + } +} diff --git a/src/Http/Controllers/CP/API/TemplatesController.php b/src/Http/Controllers/CP/API/TemplatesController.php index 7ed3fde0f3..b5c8bdb63c 100644 --- a/src/Http/Controllers/CP/API/TemplatesController.php +++ b/src/Http/Controllers/CP/API/TemplatesController.php @@ -3,8 +3,8 @@ namespace Statamic\Http\Controllers\CP\API; use RecursiveCallbackFilterIterator; -use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use Statamic\Filesystem\RecursiveDirectoryIterator; use Statamic\Http\Controllers\CP\CpController; use Statamic\Support\Str; From 5513a2793a2109c43bb56608e02f1bc1b8d5954c Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Sun, 9 Aug 2026 00:30:27 -0400 Subject: [PATCH 20/20] Remove the stat calls from the test directory cleanup is_link() lstat's the path, and PHP caches an lstat result as the stat result when it decides the path isn't a link. A junction isn't reported as a link, so the following is_dir() was served that bogus mode and answered false, sending junctions to unlink(), which cannot remove them on Windows. Try unlink() then rmdir() and recurse into whatever survives both, so no stat is involved. Cover it with a test, since a teardown leak is otherwise silent. Co-Authored-By: Claude Opus 5 (1M context) --- tests/DeletesDirectories.php | 23 +++++--------- tests/DeletesDirectoriesTest.php | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 tests/DeletesDirectoriesTest.php diff --git a/tests/DeletesDirectories.php b/tests/DeletesDirectories.php index 14ccc727e9..c6c976a7a1 100644 --- a/tests/DeletesDirectories.php +++ b/tests/DeletesDirectories.php @@ -18,24 +18,17 @@ protected function deleteDirectory(string $directory): void foreach (new FilesystemIterator($directory, FilesystemIterator::SKIP_DOTS) as $item) { $path = $item->getPathname(); - if (is_link($path)) { - @unlink($path) || @rmdir($path); - - continue; - } - - if (! is_dir($path)) { - @unlink($path); - + // Deliberately no is_dir()/is_link() calls. A junction reports an lstat + // mode that is neither, and PHP caches an lstat result as the stat result + // when it decides the path isn't a link, so the two answers contradict + // each other. unlink() removes files and file symlinks, rmdir() removes + // empty directories, directory symlinks and junctions without touching + // what they point at, and anything surviving both has contents in it. + if (@unlink($path) || @rmdir($path)) { continue; } - // A junction has neither a link nor a directory lstat mode, so it is - // indistinguishable from a directory here. rmdir() removes an empty - // directory or a junction, and leaves the junction's target alone. - if (! @rmdir($path)) { - $this->deleteDirectory($path); - } + $this->deleteDirectory($path); } @rmdir($directory); diff --git a/tests/DeletesDirectoriesTest.php b/tests/DeletesDirectoriesTest.php new file mode 100644 index 0000000000..9ba00365b4 --- /dev/null +++ b/tests/DeletesDirectoriesTest.php @@ -0,0 +1,51 @@ +dir = __DIR__.'/deletes-directories-tmp'; + } + + public function tearDown(): void + { + $this->deleteDirectory($this->dir); + + parent::tearDown(); + } + + #[Test] + public function it_deletes_a_directory_containing_links_without_touching_their_targets() + { + File::put($this->dir.'/target-dir/kept.html', ''); + File::put($this->dir.'/target-file.html', ''); + + File::put($this->dir.'/subject/file.html', ''); + File::put($this->dir.'/subject/nested/deep.html', ''); + File::makeDirectory($this->dir.'/subject/empty'); + + // A directory link is a junction on Windows, where neither unlink() nor an + // is_dir()/is_link() check behaves the way it does everywhere else. + app('files')->link($this->dir.'/target-dir', $this->dir.'/subject/linked-dir'); + app('files')->link($this->dir.'/target-file.html', $this->dir.'/subject/linked-file.html'); + + $this->deleteDirectory($this->dir.'/subject'); + + clearstatcache(); + + $this->assertFalse(is_dir($this->dir.'/subject')); + $this->assertTrue(is_file($this->dir.'/target-dir/kept.html')); + $this->assertTrue(is_file($this->dir.'/target-file.html')); + } +}